mirror of
https://github.com/rosenpass/rosenpass.git
synced 2025-12-18 21:34:37 +03:00
Compare commits
7 Commits
macos-runn
...
dev/broker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c539af8696 | ||
|
|
c4e56b890f | ||
|
|
f07fedabc5 | ||
|
|
058069e41f | ||
|
|
3a4df6d41b | ||
|
|
a7a2ddb982 | ||
|
|
83d3e39dc3 |
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env nu
|
||||
|
||||
use std log
|
||||
use log *
|
||||
|
||||
# cd to git root
|
||||
cd (git rev-parse --show-toplevel)
|
||||
@@ -116,7 +116,6 @@ for system in ($targets | columns) {
|
||||
} }
|
||||
| filter {|it| $it.needed}
|
||||
| each {|it| job-id $system $it.name}
|
||||
| sort
|
||||
)
|
||||
|
||||
mut new_job = {
|
||||
@@ -198,4 +197,4 @@ $cachix_workflow | to yaml | save --force .github/workflows/nix.yaml
|
||||
$release_workflow | to yaml | save --force .github/workflows/release.yaml
|
||||
|
||||
log info "prettify generated yaml"
|
||||
prettier -w .github/workflows/
|
||||
prettier -w .github/workflows/
|
||||
@@ -1 +0,0 @@
|
||||
FROM ghcr.io/xtruder/nix-devcontainer:v1
|
||||
@@ -1,33 +0,0 @@
|
||||
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/docker-existing-dockerfile
|
||||
{
|
||||
"name": "devcontainer-project",
|
||||
"dockerFile": "Dockerfile",
|
||||
"context": "${localWorkspaceFolder}",
|
||||
"build": {
|
||||
"args": {
|
||||
"USER_UID": "${localEnv:USER_UID}",
|
||||
"USER_GID": "${localEnv:USER_GID}"
|
||||
}
|
||||
},
|
||||
|
||||
// run arguments passed to docker
|
||||
"runArgs": ["--security-opt", "label=disable"],
|
||||
|
||||
// disable command overriding and updating remote user ID
|
||||
"overrideCommand": false,
|
||||
"userEnvProbe": "loginShell",
|
||||
"updateRemoteUserUID": false,
|
||||
|
||||
// build development environment on creation, make sure you already have shell.nix
|
||||
"onCreateCommand": "nix develop",
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [],
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["rust-lang.rust-analyzer", "tamasfe.even-better-toml"]
|
||||
}
|
||||
}
|
||||
}
|
||||
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
examples/
|
||||
target/
|
||||
flake.*
|
||||
.ci
|
||||
.direnv
|
||||
.git
|
||||
.github
|
||||
14
.github/codecov.yml
vendored
14
.github/codecov.yml
vendored
@@ -1,14 +0,0 @@
|
||||
codecov:
|
||||
branch: main
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# basic
|
||||
target: auto #default
|
||||
threshold: 5
|
||||
base: auto
|
||||
if_ci_failed: error #success, failure, error, ignore
|
||||
informational: false
|
||||
only_pulls: true
|
||||
patch: off
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
63
.github/workflows/dependent-issues.yml
vendored
63
.github/workflows/dependent-issues.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Dependent Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- closed
|
||||
- reopened
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- closed
|
||||
- reopened
|
||||
# Makes sure we always add status check for PRs. Useful only if
|
||||
# this action is required to pass before merging. Otherwise, it
|
||||
# can be removed.
|
||||
- synchronize
|
||||
|
||||
# Schedule a daily check. Useful if you reference cross-repository
|
||||
# issues or pull requests. Otherwise, it can be removed.
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
check:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: z0al/dependent-issues@v1
|
||||
env:
|
||||
# (Required) The token to use to make API calls to GitHub.
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# (Optional) The token to use to make API calls to GitHub for remote repos.
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
|
||||
with:
|
||||
# (Optional) The label to use to mark dependent issues
|
||||
label: dependent
|
||||
|
||||
# (Optional) Enable checking for dependencies in issues.
|
||||
# Enable by setting the value to "on". Default "off"
|
||||
check_issues: off
|
||||
|
||||
# (Optional) Ignore dependabot PRs.
|
||||
# Enable by setting the value to "on". Default "off"
|
||||
ignore_dependabot: off
|
||||
|
||||
# (Optional) A comma-separated list of keywords. Default
|
||||
# "depends on, blocked by"
|
||||
keywords: depends on, blocked by
|
||||
|
||||
# (Optional) A custom comment body. It supports `{{ dependencies }}` token.
|
||||
comment: >
|
||||
This PR/issue depends on:
|
||||
|
||||
{{ dependencies }}
|
||||
|
||||
By **[Dependent Issues](https://github.com/z0al/dependent-issues)** (🤖). Happy coding!
|
||||
124
.github/workflows/nix.yaml
vendored
124
.github/workflows/nix.yaml
vendored
@@ -95,7 +95,6 @@ jobs:
|
||||
- macos-13
|
||||
needs:
|
||||
- x86_64-darwin---rosenpass
|
||||
- x86_64-darwin---rp
|
||||
- x86_64-darwin---rosenpass-oci-image
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -124,22 +123,6 @@ jobs:
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.x86_64-darwin.rosenpass --print-build-logs
|
||||
x86_64-darwin---rp:
|
||||
name: Build x86_64-darwin.rp
|
||||
runs-on:
|
||||
- macos-13
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.x86_64-darwin.rp --print-build-logs
|
||||
x86_64-darwin---rosenpass-oci-image:
|
||||
name: Build x86_64-darwin.rosenpass-oci-image
|
||||
runs-on:
|
||||
@@ -227,9 +210,8 @@ jobs:
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
needs:
|
||||
- x86_64-linux---rosenpass-static
|
||||
- x86_64-linux---rosenpass-static-oci-image
|
||||
- x86_64-linux---rp-static
|
||||
- x86_64-linux---rosenpass-static
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
@@ -241,30 +223,6 @@ jobs:
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.x86_64-linux.release-package --print-build-logs
|
||||
aarch64-linux---release-package:
|
||||
name: Build aarch64-linux.release-package
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
needs:
|
||||
- aarch64-linux---rosenpass-oci-image
|
||||
- aarch64-linux---rosenpass
|
||||
- aarch64-linux---rp
|
||||
steps:
|
||||
- run: |
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update -q -y && sudo apt-get install -q -y qemu-system-aarch64 qemu-efi binfmt-support qemu-user-static
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
extra_nix_config: |
|
||||
system = aarch64-linux
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.aarch64-linux.release-package --print-build-logs
|
||||
x86_64-linux---rosenpass:
|
||||
name: Build x86_64-linux.rosenpass
|
||||
runs-on:
|
||||
@@ -281,48 +239,6 @@ jobs:
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.x86_64-linux.rosenpass --print-build-logs
|
||||
aarch64-linux---rosenpass:
|
||||
name: Build aarch64-linux.rosenpass
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- run: |
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update -q -y && sudo apt-get install -q -y qemu-system-aarch64 qemu-efi binfmt-support qemu-user-static
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
extra_nix_config: |
|
||||
system = aarch64-linux
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.aarch64-linux.rosenpass --print-build-logs
|
||||
aarch64-linux---rp:
|
||||
name: Build aarch64-linux.rp
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- run: |
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update -q -y && sudo apt-get install -q -y qemu-system-aarch64 qemu-efi binfmt-support qemu-user-static
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
extra_nix_config: |
|
||||
system = aarch64-linux
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.aarch64-linux.rp --print-build-logs
|
||||
x86_64-linux---rosenpass-oci-image:
|
||||
name: Build x86_64-linux.rosenpass-oci-image
|
||||
runs-on:
|
||||
@@ -340,28 +256,6 @@ jobs:
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.x86_64-linux.rosenpass-oci-image --print-build-logs
|
||||
aarch64-linux---rosenpass-oci-image:
|
||||
name: Build aarch64-linux.rosenpass-oci-image
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
needs:
|
||||
- aarch64-linux---rosenpass
|
||||
steps:
|
||||
- run: |
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update -q -y && sudo apt-get install -q -y qemu-system-aarch64 qemu-efi binfmt-support qemu-user-static
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
extra_nix_config: |
|
||||
system = aarch64-linux
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.aarch64-linux.rosenpass-oci-image --print-build-logs
|
||||
x86_64-linux---rosenpass-static:
|
||||
name: Build x86_64-linux.rosenpass-static
|
||||
runs-on:
|
||||
@@ -378,22 +272,6 @@ jobs:
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.x86_64-linux.rosenpass-static --print-build-logs
|
||||
x86_64-linux---rp-static:
|
||||
name: Build x86_64-linux.rp-static
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build
|
||||
run: nix build .#packages.x86_64-linux.rp-static --print-build-logs
|
||||
x86_64-linux---rosenpass-static-oci-image:
|
||||
name: Build x86_64-linux.rosenpass-static-oci-image
|
||||
runs-on:
|
||||
|
||||
61
.github/workflows/qc.yaml
vendored
61
.github/workflows/qc.yaml
vendored
@@ -46,22 +46,12 @@ jobs:
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Install libsodium
|
||||
run: sudo apt-get install -y libsodium-dev
|
||||
# liboqs requires quite a lot of stack memory, thus we adjust
|
||||
# the default stack size picked for new threads (which is used
|
||||
# by `cargo test`) to be _big enough_. Setting it to 8 MiB
|
||||
- run: RUST_MIN_STACK=8388608 cargo bench --workspace --exclude rosenpass-fuzzing
|
||||
|
||||
mandoc:
|
||||
name: mandoc
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install mandoc
|
||||
run: sudo apt-get install -y mandoc
|
||||
- uses: actions/checkout@v3
|
||||
- name: Check rosenpass.1
|
||||
run: doc/check.sh doc/rosenpass.1
|
||||
- name: Check rp.1
|
||||
run: doc/check.sh doc/rp.1
|
||||
- run: RUST_MIN_STACK=8388608 cargo bench --no-run --workspace
|
||||
|
||||
cargo-audit:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -85,6 +75,8 @@ jobs:
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- run: rustup component add clippy
|
||||
- name: Install libsodium
|
||||
run: sudo apt-get install -y libsodium-dev
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -104,18 +96,15 @@ jobs:
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- run: rustup component add clippy
|
||||
- name: Install libsodium
|
||||
run: sudo apt-get install -y libsodium-dev
|
||||
# `--no-deps` used as a workaround for a rust compiler bug. See:
|
||||
# - https://github.com/rosenpass/rosenpass/issues/62
|
||||
# - https://github.com/rust-lang/rust/issues/108378
|
||||
- run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --document-private-items
|
||||
|
||||
cargo-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-13]
|
||||
# - ubuntu is x86-64
|
||||
# - macos-13 is also x86-64 architecture
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
@@ -127,10 +116,12 @@ jobs:
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Install libsodium
|
||||
run: sudo apt-get install -y libsodium-dev
|
||||
# liboqs requires quite a lot of stack memory, thus we adjust
|
||||
# the default stack size picked for new threads (which is used
|
||||
# by `cargo test`) to be _big enough_. Setting it to 8 MiB
|
||||
- run: RUST_MIN_STACK=8388608 cargo test --workspace --all-features
|
||||
- run: RUST_MIN_STACK=8388608 cargo test --workspace
|
||||
|
||||
cargo-test-nix-devshell-x86_64-linux:
|
||||
runs-on:
|
||||
@@ -153,7 +144,7 @@ jobs:
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- run: nix develop --command cargo test --workspace --all-features
|
||||
- run: nix develop --command cargo test --workspace
|
||||
|
||||
cargo-fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -168,6 +159,8 @@ jobs:
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Install libsodium
|
||||
run: sudo apt-get install -y libsodium-dev
|
||||
- name: Install nightly toolchain
|
||||
run: |
|
||||
rustup toolchain install nightly
|
||||
@@ -181,27 +174,5 @@ jobs:
|
||||
cargo fuzz run fuzz_handle_msg -- -max_total_time=5
|
||||
ulimit -s 8192000 && RUST_MIN_STACK=33554432000 && cargo fuzz run fuzz_kyber_encaps -- -max_total_time=5
|
||||
cargo fuzz run fuzz_mceliece_encaps -- -max_total_time=5
|
||||
cargo fuzz run fuzz_box_secret_alloc_malloc -- -max_total_time=5
|
||||
cargo fuzz run fuzz_box_secret_alloc_memfdsec -- -max_total_time=5
|
||||
cargo fuzz run fuzz_box_secret_alloc_memfdsec_mallocfb -- -max_total_time=5
|
||||
cargo fuzz run fuzz_vec_secret_alloc_malloc -- -max_total_time=5
|
||||
cargo fuzz run fuzz_vec_secret_alloc_memfdsec -- -max_total_time=5
|
||||
cargo fuzz run fuzz_vec_secret_alloc_memfdsec_mallocfb -- -max_total_time=5
|
||||
|
||||
codecov:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: rustup component add llvm-tools-preview
|
||||
- run: |
|
||||
cargo install cargo-llvm-cov || true
|
||||
cargo llvm-cov --lcov --output-path coverage.lcov
|
||||
# If using tarapulin
|
||||
#- run: cargo install cargo-tarpaulin
|
||||
#- run: cargo tarpaulin --out Xml
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4.0.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.lcov
|
||||
verbose: true
|
||||
cargo fuzz run fuzz_box_secret_alloc -- -max_total_time=5
|
||||
cargo fuzz run fuzz_vec_secret_alloc -- -max_total_time=5
|
||||
|
||||
1463
Cargo.lock
generated
1463
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
74
Cargo.toml
74
Cargo.toml
@@ -11,11 +11,14 @@ members = [
|
||||
"to",
|
||||
"fuzz",
|
||||
"secret-memory",
|
||||
"rp",
|
||||
"lenses",
|
||||
"wireguard-broker",
|
||||
]
|
||||
|
||||
default-members = ["rosenpass", "rp", "wireguard-broker"]
|
||||
default-members = [
|
||||
"rosenpass",
|
||||
"wireguard-broker"
|
||||
]
|
||||
|
||||
[workspace.metadata.release]
|
||||
# ensure that adding `--package` as argument to `cargo release` still creates version tags in the form of `vx.y.z`
|
||||
@@ -30,53 +33,36 @@ rosenpass-ciphers = { path = "ciphers" }
|
||||
rosenpass-to = { path = "to" }
|
||||
rosenpass-secret-memory = { path = "secret-memory" }
|
||||
rosenpass-oqs = { path = "oqs" }
|
||||
rosenpass-lenses = { path = "lenses" }
|
||||
rosenpass-wireguard-broker = { path = "wireguard-broker" }
|
||||
criterion = "0.4.0"
|
||||
test_bin = "0.4.0"
|
||||
libfuzzer-sys = "0.4"
|
||||
stacker = "0.1.15"
|
||||
doc-comment = "0.3.3"
|
||||
base64ct = {version = "1.6.0", default-features=false}
|
||||
zeroize = "1.8.1"
|
||||
memoffset = "0.9.1"
|
||||
thiserror = "1.0.61"
|
||||
paste = "1.0.15"
|
||||
env_logger = "0.10.2"
|
||||
base64 = "0.21.5"
|
||||
zeroize = "1.7.0"
|
||||
memoffset = "0.9.0"
|
||||
thiserror = "1.0.50"
|
||||
paste = "1.0.14"
|
||||
env_logger = "0.10.1"
|
||||
toml = "0.7.8"
|
||||
static_assertions = "1.1.0"
|
||||
allocator-api2 = "0.2.14"
|
||||
memsec = { git="https://github.com/rosenpass/memsec.git" ,rev="aceb9baee8aec6844125bd6612f92e9a281373df", features = [ "alloc_ext", ] }
|
||||
allocator-api2-tests = "0.2.14"
|
||||
memsec = "0.6.3"
|
||||
rand = "0.8.5"
|
||||
typenum = "1.17.0"
|
||||
log = { version = "0.4.21" }
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
arbitrary = { version = "1.3.2", features = ["derive"] }
|
||||
anyhow = { version = "1.0.86", features = ["backtrace", "std"] }
|
||||
mio = { version = "0.8.11", features = ["net", "os-poll"] }
|
||||
oqs-sys = { version = "0.9.1", default-features = false, features = [
|
||||
'classic_mceliece',
|
||||
'kyber',
|
||||
] }
|
||||
blake2 = "0.10.6"
|
||||
chacha20poly1305 = { version = "0.10.1", default-features = false, features = [
|
||||
"std",
|
||||
"heapless",
|
||||
] }
|
||||
zerocopy = { version = "0.7.34", features = ["derive"] }
|
||||
home = "0.5.9"
|
||||
derive_builder = "0.20.0"
|
||||
tokio = { version = "1.38", features = ["macros", "rt-multi-thread"] }
|
||||
postcard= {version = "1.0.8", features = ["alloc"]}
|
||||
|
||||
#Dev dependencies
|
||||
serial_test = "3.1.1"
|
||||
tempfile = "3"
|
||||
stacker = "0.1.15"
|
||||
libfuzzer-sys = "0.4"
|
||||
test_bin = "0.4.0"
|
||||
criterion = "0.4.0"
|
||||
allocator-api2-tests = "0.2.15"
|
||||
procspawn = {version = "1.0.0", features= ["test-support"]}
|
||||
|
||||
|
||||
#Broker dependencies (might need cleanup or changes)
|
||||
wireguard-uapi = { version = "3.0.0", features = ["xplatform"] }
|
||||
wireguard-uapi = "3.0.0"
|
||||
command-fds = "0.2.3"
|
||||
rustix = { version = "0.38.27", features = ["net"] }
|
||||
tokio = { version = "1.34.0", features = ["sync", "full", "mio"] }
|
||||
typenum = "1.17.0"
|
||||
log = { version = "0.4.20" }
|
||||
clap = { version = "4.4.10", features = ["derive"] }
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
arbitrary = { version = "1.3.2", features = ["derive"] }
|
||||
anyhow = { version = "1.0.75", features = ["backtrace", "std"] }
|
||||
mio = { version = "0.8.9", features = ["net", "os-poll"] }
|
||||
oqs-sys = { version = "0.8", default-features = false, features = ['classic_mceliece', 'kyber'] }
|
||||
blake2 = "0.10.6"
|
||||
chacha20poly1305 = { version = "0.10.1", default-features = false, features = [ "std", "heapless" ] }
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
In this case the test uses secure rng and a fresh secure biscuit key.
|
||||
*/
|
||||
|
||||
|
||||
#include "config.mpv"
|
||||
|
||||
#define CHAINING_KEY_EVENTS 1
|
||||
@@ -43,6 +44,7 @@
|
||||
#include "rosenpass/oracles.mpv"
|
||||
#include "crypto/kem.mpv"
|
||||
|
||||
#define INITIATOR_TEST
|
||||
#define NEW_TRUSTED_SEED(name) \
|
||||
new MCAT(name, _secret_seed):seed_prec; \
|
||||
name <- make_trusted_seed(MCAT(name, _secret_seed)); \
|
||||
@@ -55,86 +57,52 @@ free initiator1, initiator2:kem_sk_prec.
|
||||
free responder1, responder2:kem_sk_prec.
|
||||
|
||||
let secure_init_hello(initiator: kem_sk_tmpl, sidi : SessionId, psk: key_tmpl, responder: kem_sk_tmpl) =
|
||||
|
||||
new epkit:kem_pk; // epki
|
||||
new sctrt:bits; // sctr
|
||||
new pidiCt:bits; // pidiC
|
||||
new autht:bits; // auth
|
||||
|
||||
NEW_TRUSTED_SEED(seski_trusted_seed)
|
||||
NEW_TRUSTED_SEED(ssptr_trusted_seed)
|
||||
new last_cookie:key;
|
||||
new call:Atom;
|
||||
|
||||
Oinitiator_inner(sidi, initiator, psk, responder, seski_trusted_seed, ssptr_trusted_seed, last_cookie, D, call).
|
||||
|
||||
let secure_resp_hello(initiator: kem_sk_tmpl, responder: kem_sk_tmpl, sidi:SessionId, sidr:SessionId, biscuit_no:Atom, psk:key_tmpl) =
|
||||
|
||||
in(D, InitHello(=secure_sidi, epki, sctr, pidiC, auth));
|
||||
Oinitiator_inner(sidi, initiator, psk, responder, seski_trusted_seed, ssptr_trusted_seed, D).
|
||||
|
||||
let secure_resp_hello(initiator: kem_sk_tmpl, responder: kem_sk_tmpl, sidr:SessionId, sidi:SessionId, biscuit_no:Atom, psk:key_tmpl) =
|
||||
in(D, Envelope(k, IH2b(InitHello(=sidi, epki, sctr, pidiC, auth))));
|
||||
ih <- InitHello(sidi, epki, sctr, pidiC, auth);
|
||||
NEW_TRUSTED_SEED(septi_trusted_seed)
|
||||
NEW_TRUSTED_SEED(sspti_trusted_seed)
|
||||
new last_cookie:key;
|
||||
new call:Atom;
|
||||
|
||||
Oinit_hello_inner(sidr, biscuit_no, responder, psk, initiator, septi_trusted_seed, sspti_trusted_seed, ih, last_cookie, D, call).
|
||||
Oinit_hello_inner(sidr, biscuit_no, responder, psk, initiator, septi_trusted_seed, sspti_trusted_seed, ih, D).
|
||||
|
||||
let secure_init_conf(initiator: kem_sk_tmpl, responder: kem_sk_tmpl, psk:key_tmpl, sidi:SessionId, sidr:SessionId) =
|
||||
in(D, InitConf(=sidi, =sidr, biscuit, auth3));
|
||||
in(D, Envelope(k3, IC2b(InitConf(=sidi, =sidr, biscuit, auth3))));
|
||||
ic <- InitConf(sidi,sidr,biscuit, auth3);
|
||||
NEW_TRUSTED_SEED(seski_trusted_seed)
|
||||
NEW_TRUSTED_SEED(ssptr_trusted_seed)
|
||||
new last_cookie:key;
|
||||
call <- Cinit_conf(initiator, psk, responder, ic);
|
||||
Oinit_conf_inner(initiator, psk, responder, ic).
|
||||
|
||||
Oinit_conf_inner(initiator, psk, responder, ic, call).
|
||||
|
||||
let secure_communication(initiator: kem_sk_tmpl, responder:kem_sk_tmpl, key:key) =
|
||||
key_tmpl <- prepare_key(key);
|
||||
(!secure_init_hello(initiator, secure_sidi, key_tmpl, responder))
|
||||
| !secure_resp_hello(initiator, responder, secure_sidi, secure_sidr, secure_biscuit_no, key_tmpl)
|
||||
| !(secure_init_conf(initiator, responder, key_tmpl, secure_sidi, secure_sidr)).
|
||||
|
||||
let participant_communication_initiator(participant:kem_sk_tmpl) =
|
||||
in(C, responder:kem_sk_tmpl);
|
||||
in(C, k:key);
|
||||
secure_communication(participant, responder, k).
|
||||
|
||||
let participant_communication_responder(participant:kem_sk_tmpl) =
|
||||
in(C, initiator:kem_sk_tmpl);
|
||||
in(C, k:key);
|
||||
secure_communication(initiator, participant, k).
|
||||
|
||||
let participants_communication() =
|
||||
initiator1_tmpl <- make_trusted_kem_sk(initiator1);
|
||||
initiator2_tmpl <- make_trusted_kem_sk(initiator2);
|
||||
responder1_tmpl <- make_trusted_kem_sk(responder1);
|
||||
responder2_tmpl <- make_trusted_kem_sk(responder2);
|
||||
|
||||
!participant_communication_initiator(initiator1_tmpl) | !participant_communication_responder(initiator1_tmpl)
|
||||
| !participant_communication_initiator(initiator2_tmpl) | !participant_communication_responder(initiator2_tmpl)
|
||||
| !participant_communication_initiator(responder1_tmpl) | !participant_communication_responder(responder1_tmpl)
|
||||
| !participant_communication_initiator(responder2_tmpl) | !participant_communication_responder(responder2_tmpl).
|
||||
let secure_communication(initiator: kem_sk_tmpl, responder:kem_sk_tmpl) =
|
||||
secure_key <- prepare_key(secure_psk);
|
||||
(!secure_init_hello(initiator, secure_sidi, secure_key, responder))
|
||||
| !secure_resp_hello(initiator, responder, secure_sidr, secure_sidi, secure_biscuit_no, secure_key)
|
||||
| !(secure_init_conf(initiator, responder, secure_key, secure_sidi, secure_sidr)).
|
||||
|
||||
let pipeChannel(D:channel, C:channel) =
|
||||
in(D, b:bits);
|
||||
out(C, b).
|
||||
|
||||
let secretCommunication() =
|
||||
fun kem_private(kem_pk): kem_sk
|
||||
reduc forall sk_tmpl:kem_sk;
|
||||
kem_private(kem_pub(sk_tmpl)) = sk_tmpl[private].
|
||||
|
||||
let secretCommunication() =
|
||||
#ifdef INITIATOR_TEST
|
||||
initiator_seed <- choice[make_trusted_kem_sk(initiator1), make_trusted_kem_sk(initiator2)];
|
||||
initiator_pk <- choice[setup_kem_pk(make_trusted_kem_sk(initiator1)), setup_kem_pk(make_trusted_kem_sk(initiator2))];
|
||||
initiator_seed <- prepare_kem_sk(kem_private(initiator_pk));
|
||||
#else
|
||||
initiator_seed <- make_trusted_kem_sk(initiator1);
|
||||
initiator_seed <- prepare_kem_sk(trusted_kem_sk(initiator1));
|
||||
#endif
|
||||
#ifdef RESPONDER_TEST
|
||||
responder_seed <- choice[make_trusted_kem_sk(responder1), make_trusted_kem_sk(responder2)];
|
||||
responder_pk <- choice[setup_kem_pk(make_trusted_kem_sk(responder1)), setup_kem_pk(make_trusted_kem_sk(responder2))];
|
||||
responder_seed <- prepare_kem_sk(kem_private(responder_pk));
|
||||
#else
|
||||
responder_seed <- make_trusted_kem_sk(responder1);
|
||||
responder_seed <- prepare_kem_sk(trusted_kem_sk(responder1));
|
||||
#endif
|
||||
|
||||
secure_communication(initiator_seed, responder_seed, secure_psk) | !pipeChannel(D, C).
|
||||
secure_communication(initiator_seed, responder_seed) | !pipeChannel(D, C).
|
||||
|
||||
let reveal_pks() =
|
||||
out(C, setup_kem_pk(make_trusted_kem_sk(responder1)));
|
||||
@@ -148,8 +116,6 @@ let rosenpass_main2() =
|
||||
| REP(RESPONDER_BOUND, Oinit_conf).
|
||||
|
||||
let identity_hiding_main() =
|
||||
0 | reveal_pks() | rosenpass_main2() | participants_communication() | phase 1; secretCommunication().
|
||||
0 | reveal_pks() | rosenpass_main2() | phase 1; secretCommunication().
|
||||
|
||||
#ifndef CUSTOM_MAIN
|
||||
let main = identity_hiding_main.
|
||||
#endif
|
||||
@@ -1,25 +0,0 @@
|
||||
#define INITIATOR_TEST 1
|
||||
|
||||
#include "rosenpass/03_identity_hiding.mpv"
|
||||
|
||||
// nounif a:Atom, s:seed, a2:Atom;
|
||||
// ConsumeSeed(a, s, a2) / 6300[conclusion].
|
||||
|
||||
nounif v:seed_prec; attacker(prepare_seed(trusted_seed( v )))/6217[hypothesis].
|
||||
nounif v:seed; attacker(prepare_seed( v ))/6216[hypothesis].
|
||||
nounif v:seed; attacker(rng_kem_sk( v ))/6215[hypothesis].
|
||||
nounif v:seed; attacker(rng_key( v ))/6214[hypothesis].
|
||||
nounif v:key_prec; attacker(prepare_key(trusted_key( v )))/6213[hypothesis].
|
||||
nounif v:kem_sk_prec; attacker(prepare_kem_sk(trusted_kem_sk( v )))/6212[hypothesis].
|
||||
nounif v:key; attacker(prepare_key( v ))/6211[hypothesis].
|
||||
nounif v:kem_sk; attacker(prepare_kem_sk( v ))/6210[hypothesis].
|
||||
nounif Spk:kem_sk_tmpl;
|
||||
attacker(Creveal_kem_pk(Spk))/6110[conclusion].
|
||||
nounif sid:SessionId, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, Seski:seed_tmpl, Ssptr:seed_tmpl;
|
||||
attacker(Cinitiator( *sid, *Ssskm, *Spsk, *Sspkt, *Seski, *Ssptr ))/6109[conclusion].
|
||||
nounif sid:SessionId, biscuit_no:Atom, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, Septi:seed_tmpl, Sspti:seed_tmpl, ih:InitHello_t;
|
||||
attacker(Cinit_hello( *sid, *biscuit_no, *Ssskm, *Spsk, *Sspkt, *Septi, *Sspti, *ih ))/6108[conclusion].
|
||||
nounif rh:RespHello_t;
|
||||
attacker(Cresp_hello( *rh ))/6107[conclusion].
|
||||
nounif Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, ic:InitConf_t;
|
||||
attacker(Cinit_conf( *Ssskm, *Spsk, *Sspkt, *ic ))/6106[conclusion].
|
||||
@@ -1,96 +0,0 @@
|
||||
#define RESPONDER_TEST 1
|
||||
|
||||
#include "rosenpass/03_identity_hiding.mpv"
|
||||
|
||||
// select k:kem_pk,ih: InitHello_t; attacker(prf(prf(prf(prf(key0, PROTOCOL), MAC), kem_pk2b(k) ), IH2b(ih))) phase 1/6300[hypothesis].
|
||||
|
||||
// select epki:kem_pk, sctr:bits, pidiC:bits, auth:bits, epki2:kem_pk, sctr2:bits, pidiC2:bits, auth2:bits;
|
||||
// mess(D, prf(prf(prf(prf(key0,PROTOCOL),MAC),kem_pk2b(kem_pub(trusted_kem_sk(responder1)))),
|
||||
// IH2b(InitHello(secure_sidi, *epki, *sctr, *pidiC, *auth)))
|
||||
// ) [hypothesis, conclusion].
|
||||
|
||||
// select epki:kem_pk, sctr:bits, pidiC:bits, auth:bits, epki2:kem_pk, sctr2:bits, pidiC2:bits, auth2:bits;
|
||||
// attacker(choice[prf(prf(prf(prf(key0,PROTOCOL),MAC),kem_pk2b(kem_pub(trusted_kem_sk(responder1)))),
|
||||
// IH2b(InitHello(secure_sidi, *epki, *sctr, *pidiC, *auth))),
|
||||
|
||||
// prf(prf(prf(prf(key0,PROTOCOL),MAC),kem_pk2b(kem_pub(trusted_kem_sk(responder2)))),
|
||||
// IH2b(InitHello(secure_sidi, *epki2, *sctr2, *pidiC2, *auth2)))]
|
||||
// ) [hypothesis, conclusion].
|
||||
|
||||
// select
|
||||
// attacker(prf(prf(key0,PROTOCOL),MAC)) [hypothesis, conclusion].
|
||||
|
||||
// select
|
||||
// attacker(prf(key0,PROTOCOL)) [conclusion].
|
||||
|
||||
// select
|
||||
// attacker(key0) [conclusion].
|
||||
|
||||
// select
|
||||
// attacker(PROTOCOL) [conclusion].
|
||||
|
||||
// select
|
||||
// attacker(kem_pub(trusted_kem_sk(responder1))) /9999 [hypothesis, conclusion].
|
||||
|
||||
// select
|
||||
// attacker(kem_pub(trusted_kem_sk(responder2))) /9999 [hypothesis, conclusion].
|
||||
|
||||
// nounif ih:InitHello_t;
|
||||
// attacker(ih) / 9999 [hypothesis].
|
||||
|
||||
// nounif rh:RespHello_t;
|
||||
// attacker(rh) / 9999 [hypothesis].
|
||||
|
||||
// nounif ic:InitConf_t;
|
||||
// attacker(ic) / 9999 [hypothesis].
|
||||
|
||||
// nounif k:key;
|
||||
// attacker(ck_hs_enc( *k )) [hypothesis, conclusion].
|
||||
|
||||
// nounif k:key;
|
||||
// attacker(ck_hs_enc( *k )) phase 1 [hypothesis, conclusion].
|
||||
|
||||
// nounif k:key, b:bits;
|
||||
// attacker(ck_mix( *k , *b )) [hypothesis, conclusion].
|
||||
|
||||
// nounif k:key, b:bits;
|
||||
// attacker(ck_mix( *k , *b ))phase 1 [hypothesis, conclusion].
|
||||
|
||||
// // select k:kem_pk, epki2:kem_pk, sctr2:bits, pidiC2:bits, auth2:bits, epki:kem_pk, sctr:bits, pidiC:bits, auth:bits;
|
||||
// // attacker(choice[Envelope(prf(prf(prf(prf(key0,PROTOCOL),MAC),kem_pub(trusted_kem_sk(responder1))),
|
||||
// // InitHello(secure_sidi, *epki2, *sctr2, *pidiC2, *auth2)
|
||||
// // ), InitHello(secure_sidi, *epki2, *sctr2, *pidiC2, *auth2))
|
||||
// // Envelope(prf(prf(prf(prf(key0,PROTOCOL),MAC),kem_pub(trusted_kem_sk(responder2))),
|
||||
// // InitHello(secure_sidi, *epki, *sctr, *pidiC, *auth)),
|
||||
// // InitHello(secure_sidi, *epki, *sctr, *pidiC, *auth))
|
||||
// // ]) / 9999[hypothesis, conclusion].
|
||||
|
||||
// nounif k:key, b1:bits, b2:bits;
|
||||
// attacker(xaead_enc( *k, *b1, *b2)) / 9999[hypothesis,conclusion].
|
||||
|
||||
// nounif pk:kem_pk, k:key;
|
||||
// attacker(kem_enc( *pk , *k )) / 9999[hypothesis,conclusion].
|
||||
|
||||
// nounif sid:SessionId, biscuit_no:Atom, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, Septi:seed_tmpl, Sspti:seed_tmpl, ih:InitHello_t;
|
||||
// attacker(Cinit_hello( *sid, *biscuit_no, *Ssskm, *Spsk, *Sspkt, *Septi, *Sspti, *ih ))/9999[hypothesis, conclusion].
|
||||
// nounif Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, ic:InitConf_t;
|
||||
// attacker(Cinit_conf( *Ssskm, *Spsk, *Sspkt, *ic ))/9999[hypothesis, conclusion].
|
||||
// nounif sid:SessionId, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, Seski:seed_tmpl, Ssptr:seed_tmpl;
|
||||
// attacker(Cinitiator( *sid, *Ssskm, *Spsk, *Sspkt, *Seski, *Ssptr )) /9999 [hypothesis, conclusion].
|
||||
|
||||
// nounif sid:SessionId, biscuit_no:Atom, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, Septi:seed_tmpl, Sspti:seed_tmpl, ih:InitHello_t;
|
||||
// mess(C, Cinit_hello( *sid, *biscuit_no, *Ssskm, *Spsk, *Sspkt, *Septi, *Sspti, *ih ))/9999[hypothesis, conclusion].
|
||||
// nounif Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, ic:InitConf_t;
|
||||
// mess(C, Cinit_conf( *Ssskm, *Spsk, *Sspkt, *ic ))/9999[hypothesis, conclusion].
|
||||
// nounif sid:SessionId, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, Seski:seed_tmpl, Ssptr:seed_tmpl;
|
||||
// mess(C, Cinitiator( *sid, *Ssskm, *Spsk, *Sspkt, *Seski, *Ssptr )) /9999 [hypothesis, conclusion].
|
||||
// nounif rh:RespHello_t;
|
||||
// attacker(Cresp_hello( *rh ))[conclusion].
|
||||
// nounif v:seed_prec; attacker(prepare_seed(trusted_seed( v )))/6217[hypothesis].
|
||||
// nounif v:seed; attacker(prepare_seed( v ))/6216[hypothesis].
|
||||
// nounif v:seed; attacker(rng_kem_sk( v ))/6215[hypothesis].
|
||||
// nounif v:seed; attacker(rng_key( v ))/6214[hypothesis].
|
||||
// nounif v:key_prec; attacker(prepare_key(trusted_key( v )))/6213[hypothesis].
|
||||
// nounif v:kem_sk_prec; attacker(prepare_kem_sk(trusted_kem_sk( v )))/6212[hypothesis].
|
||||
// nounif v:key; attacker(prepare_key( v ))/6211[hypothesis].
|
||||
// nounif v:kem_sk; attacker(prepare_kem_sk( v ))/6210[hypothesis].
|
||||
@@ -1,29 +0,0 @@
|
||||
#define INITIATOR_TEST 1
|
||||
#define CUSTOM_MAIN 1
|
||||
|
||||
#include "rosenpass/03_identity_hiding.mpv"
|
||||
|
||||
let Oinitiator_bad_actor_inner(sk_tmp:kem_sk_prec) =
|
||||
|
||||
in(C, Cinitiator(sidi, Ssskm, Spsk, Sspkt, Seski, Ssptr));
|
||||
|
||||
#if RANDOMIZED_CALL_IDS
|
||||
new call:Atom;
|
||||
#else
|
||||
call <- Cinitiator(sidi, Ssskm, Spsk, Sspkt, Seski, Ssptr);
|
||||
#endif
|
||||
|
||||
in(C, last_cookie:key);
|
||||
tmpl <- make_trusted_kem_sk(sk_tmp);
|
||||
out(C, setup_kem_sk(tmpl));
|
||||
Oinitiator_inner(sidi, Ssskm, Spsk, tmpl, Seski, Ssptr, last_cookie, C, call).
|
||||
|
||||
let Oinitiator_bad_actor() =
|
||||
Oinitiator_bad_actor_inner(responder1) | Oinitiator_bad_actor_inner(responder2) | Oinitiator_bad_actor_inner(initiator1) | Oinitiator_bad_actor_inner(initiator2).
|
||||
|
||||
|
||||
let identity_hiding_main2() =
|
||||
0 | Oinitiator_bad_actor() | rosenpass_main2() | participants_communication() | phase 1; secretCommunication().
|
||||
|
||||
|
||||
let main = identity_hiding_main2.
|
||||
@@ -1,136 +0,0 @@
|
||||
#define CHAINING_KEY_EVENTS 1
|
||||
#define MESSAGE_TRANSMISSION_EVENTS 0
|
||||
#define SESSION_START_EVENTS 0
|
||||
#define RANDOMIZED_CALL_IDS 0
|
||||
#define COOKIE_EVENTS 1
|
||||
#define KEM_EVENTS 1
|
||||
|
||||
#include "config.mpv"
|
||||
#include "prelude/basic.mpv"
|
||||
#include "crypto/key.mpv"
|
||||
#include "crypto/kem.mpv"
|
||||
#include "rosenpass/handshake_state.mpv"
|
||||
|
||||
/* The cookie data structure is implemented based on the WireGuard protocol.
|
||||
* The ip and port is based purely on the public key and the implementation of the private cookie key is intended to mirror the biscuit key.
|
||||
* The code tests the response to a possible DOS attack by setting up alternative branches for the protocol
|
||||
* processes: Oinit_conf, Oinit_hello and resp_hello to simulate what happens when the responder or initiator is overloaded.
|
||||
* When under heavy load a valid cookie is required. When such a cookie is not present a cookie message is sent as a response.
|
||||
* Queries then test to make sure that expensive KEM operations are only conducted after a cookie has been successfully validated.
|
||||
*/
|
||||
|
||||
type CookieMsg_t.
|
||||
fun CookieMsg(
|
||||
SessionId, // sender
|
||||
bits, // nonce
|
||||
bits // cookie
|
||||
) : CookieMsg_t [data].
|
||||
|
||||
#define COOKIE_EVENTS(eventLbl) \
|
||||
COOKIE_EV(event MCAT(eventLbl, _UnderLoadEV) (SessionId, SessionId, Atom).) \
|
||||
COOKIE_EV(event MCAT(eventLbl, _CookieValidated) (SessionId, SessionId, Atom).) \
|
||||
COOKIE_EV(event MCAT(eventLbl, _CookieSent) (SessionId, SessionId, Atom, CookieMsg_t).)
|
||||
|
||||
fun cookie_key(kem_sk) : key [private].
|
||||
fun ip_and_port(kem_pk):bits.
|
||||
letfun create_mac2_key(sskm:kem_sk, spkt:kem_pk) = prf(cookie_key(sskm), ip_and_port(spkt)).
|
||||
letfun create_cookie(sskm:kem_sk, spkm:kem_pk, spkt:kem_pk, nonce:bits, msg:bits) = xaead_enc(lprf2(COOKIE, kem_pk2b(spkm), nonce),
|
||||
k2b(create_mac2_key(sskm, spkm)), msg).
|
||||
|
||||
#define COOKIE_PROCESS(eventLbl, innerFunc) \
|
||||
new nonce:bits; \
|
||||
in(C, Ccookie(mac1, mac2)); \
|
||||
COOKIE_EV(event MCAT(eventLbl, _UnderLoadEV) (sidi, sidr, call);) \
|
||||
msgB <- Envelope(mac1, msg); \
|
||||
mac2_key <- create_mac2_key(sskm, spkt); \
|
||||
if k2b(create_mac2(mac2_key, msgB)) = mac2 then \
|
||||
COOKIE_EV(event MCAT(eventLbl, _CookieValidated) (sidi, sidr, call);) \
|
||||
innerFunc \
|
||||
else \
|
||||
cookie <- create_cookie(sskm, spkm, spkt, nonce, msg); \
|
||||
cookie_msg <- CookieMsg(sidi, nonce, cookie); \
|
||||
COOKIE_EV(event MCAT(eventLbl, _CookieSent) (sidi, sidr, call, cookie_msg);) \
|
||||
out(C, cookie_msg). \
|
||||
|
||||
#include "rosenpass/oracles.mpv"
|
||||
|
||||
#include "rosenpass/responder.macro"
|
||||
COOKIE_EVENTS(Oinit_conf)
|
||||
let Oinit_conf_underLoad() =
|
||||
in(C, Cinit_conf(Ssskm, Spsk, Sspkt, ic));
|
||||
in(C, last_cookie:bits);
|
||||
|
||||
msg <- IC2b(ic);
|
||||
let InitConf(sidi, sidr, biscuit, auth) = ic in
|
||||
|
||||
new call:Atom;
|
||||
|
||||
SETUP_HANDSHAKE_STATE()
|
||||
|
||||
COOKIE_PROCESS(Oinit_conf, Oinit_conf_inner(Ssskm, Spsk, Sspkt, ic, call))
|
||||
|
||||
#include "rosenpass/responder.macro"
|
||||
COOKIE_EVENTS(Oinit_hello)
|
||||
let Oinit_hello_underLoad() =
|
||||
|
||||
in(C, Cinit_hello(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih));
|
||||
in(C, Oinit_hello_last_cookie:key);
|
||||
new call:Atom;
|
||||
|
||||
msg <- IH2b(ih);
|
||||
let InitHello(sidi, epki, sctr, pidic, auth) = ih in
|
||||
SETUP_HANDSHAKE_STATE()
|
||||
|
||||
COOKIE_PROCESS(Oinit_hello, Oinit_hello_inner(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih, Oinit_hello_last_cookie, C, call))
|
||||
|
||||
let rosenpass_dos_main() = 0
|
||||
| !Oreveal_kem_pk
|
||||
| REP(INITIATOR_BOUND, Oinitiator)
|
||||
| REP(RESPONDER_BOUND, Oinit_hello)
|
||||
| REP(RESPONDER_BOUND, Oinit_conf)
|
||||
| REP(RESPONDER_BOUND, Oinit_hello_underLoad)
|
||||
| REP(RESPONDER_BOUND, Oinit_conf_underLoad).
|
||||
|
||||
let main = rosenpass_dos_main.
|
||||
|
||||
select cookie:CookieMsg_t; attacker(cookie)/6220[hypothesis].
|
||||
nounif v:key; attacker(prepare_key( v ))/6217[hypothesis].
|
||||
nounif v:seed; attacker(prepare_seed( v ))/6216[hypothesis].
|
||||
nounif v:seed; attacker(prepare_seed( v ))/6216[hypothesis].
|
||||
nounif v:seed; attacker(rng_kem_sk( v ))/6215[hypothesis].
|
||||
nounif v:seed; attacker(rng_key( v ))/6214[hypothesis].
|
||||
nounif v:kem_sk; attacker(prepare_kem_sk( v ))/6210[hypothesis].
|
||||
|
||||
// nounif Spk:kem_sk_tmpl;
|
||||
// attacker(Creveal_kem_pk(Spk))/6110[conclusion].
|
||||
// nounif sid:SessionId, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, Seski:seed_tmpl, Ssptr:seed_tmpl;
|
||||
// attacker(Cinitiator( *sid, *Ssskm, *Spsk, *Sspkt, *Seski, *Ssptr ))/6109[conclusion].
|
||||
// nounif sid:SessionId, biscuit_no:Atom, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, Septi:seed_tmpl, Sspti:seed_tmpl, ih:InitHello_t;
|
||||
// attacker(Cinit_hello( *sid, *biscuit_no, *Ssskm, *Spsk, *Sspkt, *Septi, *Sspti, *ih ))/6108[conclusion].
|
||||
nounif rh:RespHello_t;
|
||||
attacker(Cresp_hello( *rh ))/6107[conclusion].
|
||||
nounif Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, ic:InitConf_t;
|
||||
attacker(Cinit_conf( *Ssskm, *Spsk, *Sspkt, *ic ))/6106[conclusion].
|
||||
|
||||
@reachable "DOS protection: cookie sent"
|
||||
query sidi:SessionId, sidr:SessionId, call:Atom, cookieMsg:CookieMsg_t;
|
||||
event (Oinit_hello_CookieSent(sidi, sidr, call, cookieMsg)).
|
||||
|
||||
@lemma "DOS protection: Oinit_hello kem use when under load implies validated cookie"
|
||||
lemma sidi:SessionId, sidr:SessionId, call:Atom;
|
||||
event(Oinit_hello_UnderLoadEV(sidi, sidr, call))
|
||||
&& event(Oinit_hello_KemUse(sidi, sidr, call))
|
||||
==> event(Oinit_hello_CookieValidated(sidi, sidr, call)).
|
||||
|
||||
@lemma "DOS protection: Oinit_conf kem use when under load implies validated cookie"
|
||||
lemma sidi:SessionId, sidr:SessionId, call:Atom;
|
||||
event(Oinit_conf_UnderLoadEV(sidi, sidr, call))
|
||||
&& event(Oinit_conf_KemUse(sidi, sidr, call))
|
||||
==> event(Oinit_conf_CookieValidated(sidi, sidr, call)).
|
||||
|
||||
@lemma "DOS protection: Oresp_hello kem use when under load implies validated cookie"
|
||||
lemma sidi:SessionId, sidr:SessionId, call:Atom;
|
||||
event(Oresp_hello_UnderLoadEV(sidi, sidr, call))
|
||||
&& event(Oresp_hello_KemUse(sidi, sidr, call))
|
||||
==> event(Oresp_hello_CookieValidated(sidi, sidr, call)).
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
fun cookie_key(kem_sk) : key [private].
|
||||
fun ip_and_port(kem_pk):bits.
|
||||
letfun create_mac2_key(sskm:kem_sk, spkt:kem_pk) = prf(cookie_key(sskm), ip_and_port(spkt)).
|
||||
|
||||
letfun create_cookie(sskm:kem_sk, spkm:kem_pk, spkt:kem_pk, nonce:bits, msg:bits) = xaead_enc(lprf2(COOKIE, kem_pk2b(spkm), nonce),
|
||||
k2b(create_mac2_key(sskm, spkm)), msg).
|
||||
|
||||
type CookieMsg_t.
|
||||
fun CookieMsg(
|
||||
SessionId, // sender
|
||||
bits, // nonce
|
||||
bits // cookie
|
||||
) : CookieMsg_t [data].
|
||||
|
||||
|
||||
#define COOKIE_PROCESS(eventLbl, innerFunc) \
|
||||
in(C, Ccookie(mac1, mac2)); \
|
||||
COOKIE_EV(event MCAT(eventLbl, _UnderLoadEV) (spkm, spkt, last_cookie);) \
|
||||
msgB <- Envelope(mac1, RH2b(rh)); \
|
||||
mac2_key <- create_mac2_key(sskm, spkt) \
|
||||
let RespHello(sidi, sidr, ecti, scti, biscuit, auth) = rh in \
|
||||
if Envelope(mac2_key, msgB) = mac2 then \
|
||||
COOKIE_EV(event MCAT(eventLbl, _CookieValidated) (spkm, last_cookie);) \
|
||||
innerFunc \
|
||||
else \
|
||||
new nonce:bits; \
|
||||
cookie <- create_cookie(sskm, spkm, spkt, nonce, msg) \
|
||||
cookie_msg <- CookieMsg(sidi, nonce, cookie); \
|
||||
COOKIE_EV(event MCAT(eventLbl, _CookieSent) (spkm, cookie, cookie_k, cookie_msg);) \
|
||||
out(C, cookie_msg).
|
||||
|
||||
#define COOKIE_EVENTS(eventLbl) \
|
||||
COOKIE_EV(event MCAT(eventLbl, _UnderLoadEV) (kem_pk, kem_pk, bits).) \
|
||||
COOKIE_EV(event MCAT(eventLbl, _CookieValidated) (kem_pk, bits, key, CookieMsg_t).) \
|
||||
COOKIE_EV(event MCAT(eventLbl, _CookieSent) (kem_pk, bits).)
|
||||
@@ -47,14 +47,16 @@ CK_EV( event OskOinit_conf(key, key). )
|
||||
MTX_EV( event ICRjct(InitConf_t, key, kem_sk, kem_pk). )
|
||||
SES_EV( event ResponderSession(InitConf_t, key). )
|
||||
event ConsumeBiscuit(Atom, kem_sk, kem_pk, Atom).
|
||||
let Oinit_conf() =
|
||||
in(C, Cinit_conf(Ssskm, Spsk, Sspkt, ic));
|
||||
|
||||
let Oinit_conf_inner(Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, ic:InitConf_t) =
|
||||
#if RANDOMIZED_CALL_IDS
|
||||
new call:Atom;
|
||||
#else
|
||||
call <- Cinit_conf(Ssskm, Spsk, Sspkt, ic);
|
||||
#endif
|
||||
|
||||
SETUP_HANDSHAKE_STATE()
|
||||
|
||||
eski <- kem_sk0;
|
||||
epki <- kem_pk0;
|
||||
let try_ = (
|
||||
@@ -72,6 +74,10 @@ let Oinit_conf() =
|
||||
0
|
||||
#endif
|
||||
).
|
||||
|
||||
let Oinit_conf() =
|
||||
in(C, Cinit_conf(Ssskm, Spsk, Sspkt, ic));
|
||||
Oinit_conf_inner(Ssskm, Spsk, Sspkt, ic).
|
||||
|
||||
restriction biscuit_no:Atom, sskm:kem_sk, spkr:kem_pk, ad1:Atom, ad2:Atom;
|
||||
event(ConsumeBiscuit(biscuit_no, sskm, spkr, ad1)) && event(ConsumeBiscuit(biscuit_no, sskm, spkr, ad2))
|
||||
@@ -85,8 +91,8 @@ CK_EV( event OskOresp_hello(key, key, key). )
|
||||
MTX_EV( event RHRjct(RespHello_t, key, kem_sk, kem_pk). )
|
||||
MTX_EV( event ICSent(RespHello_t, InitConf_t, key, kem_sk, kem_pk). )
|
||||
SES_EV( event InitiatorSession(RespHello_t, key). )
|
||||
let Oresp_hello(HS_DECL_ARGS) =
|
||||
in(C, Cresp_hello(RespHello(sidr, =sidi, ecti, scti, biscuit, auth)));
|
||||
let Oresp_hello(HS_DECL_ARGS, C_in:channel) =
|
||||
in(C_in, Cresp_hello(RespHello(sidr, =sidi, ecti, scti, biscuit, auth)));
|
||||
rh <- RespHello(sidr, sidi, ecti, scti, biscuit, auth);
|
||||
/* try */ let ic = (
|
||||
ck_ini <- ck;
|
||||
@@ -98,7 +104,7 @@ let Oresp_hello(HS_DECL_ARGS) =
|
||||
SES_EV( event InitiatorSession(rh, osk); )
|
||||
ic
|
||||
/* success */ ) in (
|
||||
out(C, ic)
|
||||
out(C_in, Envelope(create_mac(spkt, IC2b(ic)), IC2b(ic)))
|
||||
/* fail */ ) else (
|
||||
#if MESSAGE_TRANSMISSION_EVENTS
|
||||
event RHRjct(rh, psk, sski, spkr)
|
||||
@@ -116,8 +122,8 @@ MTX_EV( event IHRjct(InitHello_t, key, kem_sk, kem_pk). )
|
||||
MTX_EV( event RHSent(InitHello_t, RespHello_t, key, kem_sk, kem_pk). )
|
||||
event ConsumeSidr(SessionId, Atom).
|
||||
event ConsumeBn(Atom, kem_sk, kem_pk, Atom).
|
||||
let Oinit_hello() =
|
||||
in(C, Cinit_hello(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih));
|
||||
|
||||
let Oinit_hello_inner(sidm:SessionId, biscuit_no:Atom, Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt: kem_sk_tmpl, Septi: seed_tmpl, Sspti: seed_tmpl, ih: InitHello_t, C_out:channel) =
|
||||
#if RANDOMIZED_CALL_IDS
|
||||
new call:Atom;
|
||||
#else
|
||||
@@ -125,14 +131,19 @@ let Oinit_hello() =
|
||||
#endif
|
||||
// TODO: This is ugly
|
||||
let InitHello(sidi, epki, sctr, pidiC, auth) = ih in
|
||||
|
||||
SETUP_HANDSHAKE_STATE()
|
||||
|
||||
eski <- kem_sk0;
|
||||
epti <- rng_key(setup_seed(Septi)); // RHR4
|
||||
spti <- rng_key(setup_seed(Sspti)); // RHR5
|
||||
|
||||
event ConsumeBn(biscuit_no, sskm, spkt, call);
|
||||
event ConsumeSidr(sidr, call);
|
||||
|
||||
epti <- rng_key(setup_seed(Septi)); // RHR4
|
||||
spti <- rng_key(setup_seed(Sspti)); // RHR5
|
||||
event ConsumeSeed(Epti, setup_seed(Septi), call);
|
||||
event ConsumeSeed(Spti, setup_seed(Sspti), call);
|
||||
|
||||
let rh = (
|
||||
INITHELLO_CONSUME()
|
||||
ck_ini <- ck;
|
||||
@@ -141,7 +152,8 @@ let Oinit_hello() =
|
||||
MTX_EV( event RHSent(ih, rh, psk, sskr, spki); )
|
||||
rh
|
||||
/* success */ ) in (
|
||||
out(C, rh)
|
||||
out(C_out, Envelope(create_mac(spkt, RH2b(rh)), RH2b(rh)))
|
||||
|
||||
/* fail */ ) else (
|
||||
#if MESSAGE_TRANSMISSION_EVENTS
|
||||
event IHRjct(ih, psk, sskr, spki)
|
||||
@@ -150,6 +162,10 @@ let Oinit_hello() =
|
||||
#endif
|
||||
).
|
||||
|
||||
let Oinit_hello() =
|
||||
in(C, Cinit_hello(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih));
|
||||
Oinit_hello_inner(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih, C).
|
||||
|
||||
restriction sid:SessionId, ad1:Atom, ad2:Atom;
|
||||
event(ConsumeSidr(sid, ad1)) && event(ConsumeSidr(sid, ad2))
|
||||
==> ad1 = ad2.
|
||||
@@ -167,26 +183,34 @@ CK_EV( event OskOinitiator_ck(key). )
|
||||
CK_EV( event OskOinitiator(key, key, kem_sk, kem_pk, key). )
|
||||
MTX_EV( event IHSent(InitHello_t, key, kem_sk, kem_pk). )
|
||||
event ConsumeSidi(SessionId, Atom).
|
||||
|
||||
let Oinitiator_inner(sidi: SessionId, Ssskm: kem_sk_tmpl, Spsk: key_tmpl, Sspkt: kem_sk_tmpl, Seski: seed_tmpl, Ssptr: seed_tmpl, C_out:channel) =
|
||||
#if RANDOMIZED_CALL_IDS
|
||||
new call:Atom;
|
||||
#else
|
||||
call <- Cinitiator(sidi, Ssskm, Spsk, Sspkt, Seski, Ssptr);
|
||||
#endif
|
||||
|
||||
SETUP_HANDSHAKE_STATE()
|
||||
|
||||
sidr <- sid0;
|
||||
|
||||
RNG_KEM_PAIR(eski, epki, Seski) // IHI3
|
||||
sptr <- rng_key(setup_seed(Ssptr)); // IHI5
|
||||
event ConsumeSidi(sidi, call);
|
||||
event ConsumeSeed(Sptr, setup_seed(Ssptr), call);
|
||||
event ConsumeSeed(Eski, setup_seed(Seski), call);
|
||||
|
||||
INITHELLO_PRODUCE()
|
||||
CK_EV( event OskOinitiator_ck(ck); )
|
||||
CK_EV( event OskOinitiator(ck, psk, sski, spkr, sptr); )
|
||||
MTX_EV( event IHSent(ih, psk, sski, spkr); )
|
||||
out(C_out, Envelope(create_mac(spkt, IH2b(ih)), IH2b(ih)));
|
||||
Oresp_hello(HS_PASS_ARGS, C_out).
|
||||
|
||||
let Oinitiator() =
|
||||
in(C, Cinitiator(sidi, Ssskm, Spsk, Sspkt, Seski, Ssptr));
|
||||
#if RANDOMIZED_CALL_IDS
|
||||
new call:Atom;
|
||||
#else
|
||||
call <- Cinitiator(sidi, Ssskm, Spsk, Sspkt, Seski, Ssptr);
|
||||
#endif
|
||||
SETUP_HANDSHAKE_STATE()
|
||||
RNG_KEM_PAIR(eski, epki, Seski) // IHI3
|
||||
sidr <- sid0;
|
||||
sptr <- rng_key(setup_seed(Ssptr)); // IHI5
|
||||
event ConsumeSidi(sidi, call);
|
||||
event ConsumeSeed(Sptr, setup_seed(Ssptr), call);
|
||||
event ConsumeSeed(Eski, setup_seed(Seski), call);
|
||||
INITHELLO_PRODUCE()
|
||||
CK_EV( event OskOinitiator_ck(ck); )
|
||||
CK_EV( event OskOinitiator(ck, psk, sski, spkr, sptr); )
|
||||
MTX_EV( event IHSent(ih, psk, sski, spkr); )
|
||||
out(C, ih);
|
||||
Oresp_hello(HS_PASS_ARGS).
|
||||
Oinitiator_inner(sidi, Ssskm, Spsk, Sspkt, Seski, Ssptr, C).
|
||||
|
||||
restriction sid:SessionId, ad1:Atom, ad2:Atom;
|
||||
event(ConsumeSidi(sid, ad1)) && event(ConsumeSidi(sid, ad2))
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
#include "crypto/kem.mpv"
|
||||
#include "rosenpass/handshake_state.mpv"
|
||||
|
||||
fun Envelope(
|
||||
key,
|
||||
bits
|
||||
): bits [data].
|
||||
letfun create_mac(pk:kem_pk, payload:bits) = lprf2(MAC, kem_pk2b(pk), payload).
|
||||
|
||||
type InitHello_t.
|
||||
fun InitHello(
|
||||
SessionId, // sidi
|
||||
@@ -11,6 +17,8 @@ fun InitHello(
|
||||
bits // auth
|
||||
) : InitHello_t [data].
|
||||
|
||||
fun IH2b(InitHello_t) : bitstring [typeConverter].
|
||||
|
||||
#define INITHELLO_PRODUCE() \
|
||||
ck <- lprf1(CK_INIT, kem_pk2b(spkr)); /* IHI1 */ \
|
||||
/* not handled here */ /* IHI2 */ \
|
||||
@@ -41,7 +49,9 @@ fun RespHello(
|
||||
bits // auth
|
||||
) : RespHello_t [data].
|
||||
|
||||
#define RESPHELLO_PRODUCE() \
|
||||
fun RH2b(RespHello_t) : bitstring [typeConverter].
|
||||
|
||||
#define RESPHELLO_PRODUCE() \
|
||||
/* not handled here */ /* RHR1 */ \
|
||||
MIX2(sid2b(sidr), sid2b(sidi)) /* RHR3 */ \
|
||||
ENCAPS_AND_MIX(ecti, epki, epti) /* RHR4 */ \
|
||||
@@ -67,6 +77,8 @@ fun InitConf(
|
||||
bits // auth
|
||||
) : InitConf_t [data].
|
||||
|
||||
fun IC2b(InitConf_t) : bitstring [typeConverter].
|
||||
|
||||
#define INITCONF_PRODUCE() \
|
||||
MIX2(sid2b(sidi), sid2b(sidr)) /* ICI3 */ \
|
||||
ENCRYPT_AND_MIX(auth, empty) /* ICI4 */ \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Rosenpass internal cryptographic traits
|
||||
# Rosenpass internal libsodium bindings
|
||||
|
||||
Rosenpass internal library providing traits for cryptographic primitives.
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ pub fn hash<'a>(key: &'a [u8], data: &'a [u8]) -> impl To<[u8], anyhow::Result<(
|
||||
// out the right way to use the imports while allowing for zeroization.
|
||||
// An API based on slices might actually be simpler.
|
||||
let mut tmp = Zeroizing::new([0u8; OUT_LEN]);
|
||||
let tmp = GenericArray::from_mut_slice(tmp.as_mut());
|
||||
h.finalize_into(tmp);
|
||||
let mut tmp = GenericArray::from_mut_slice(tmp.as_mut());
|
||||
h.finalize_into(&mut tmp);
|
||||
copy_slice(tmp.as_ref()).to(out);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn encrypt(
|
||||
let nonce = GenericArray::from_slice(nonce);
|
||||
let (ct, mac) = ciphertext.split_at_mut(ciphertext.len() - TAG_LEN);
|
||||
copy_slice(plaintext).to(ct);
|
||||
let mac_value = AeadImpl::new_from_slice(key)?.encrypt_in_place_detached(nonce, ad, ct)?;
|
||||
let mac_value = AeadImpl::new_from_slice(key)?.encrypt_in_place_detached(&nonce, ad, ct)?;
|
||||
copy_slice(&mac_value[..]).to(mac);
|
||||
Ok(())
|
||||
}
|
||||
@@ -38,6 +38,6 @@ pub fn decrypt(
|
||||
let (ct, mac) = ciphertext.split_at(ciphertext.len() - TAG_LEN);
|
||||
let tag = GenericArray::from_slice(mac);
|
||||
copy_slice(ct).to(plaintext);
|
||||
AeadImpl::new_from_slice(key)?.decrypt_in_place_detached(nonce, ad, plaintext, tag)?;
|
||||
AeadImpl::new_from_slice(key)?.decrypt_in_place_detached(&nonce, ad, plaintext, tag)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn encrypt(
|
||||
let (ct, mac) = ct_mac.split_at_mut(ct_mac.len() - TAG_LEN);
|
||||
copy_slice(nonce).to(n);
|
||||
copy_slice(plaintext).to(ct);
|
||||
let mac_value = AeadImpl::new_from_slice(key)?.encrypt_in_place_detached(nonce, ad, ct)?;
|
||||
let mac_value = AeadImpl::new_from_slice(key)?.encrypt_in_place_detached(&nonce, ad, ct)?;
|
||||
copy_slice(&mac_value[..]).to(mac);
|
||||
Ok(())
|
||||
}
|
||||
@@ -40,6 +40,6 @@ pub fn decrypt(
|
||||
let nonce = GenericArray::from_slice(n);
|
||||
let tag = GenericArray::from_slice(mac);
|
||||
copy_slice(ct).to(plaintext);
|
||||
AeadImpl::new_from_slice(key)?.decrypt_in_place_detached(nonce, ad, plaintext, tag)?;
|
||||
AeadImpl::new_from_slice(key)?.decrypt_in_place_detached(&nonce, ad, plaintext, tag)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,12 +11,6 @@ readme = "readme.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
constant_time_tests = []
|
||||
|
||||
[dependencies]
|
||||
rosenpass-to = { workspace = true }
|
||||
memsec = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.5"
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
use core::ptr;
|
||||
|
||||
/// Little endian memcmp version of quinier/memsec
|
||||
/// https://github.com/quininer/memsec/blob/bbc647967ff6d20d6dccf1c85f5d9037fcadd3b0/src/lib.rs#L30
|
||||
#[inline(never)]
|
||||
pub unsafe fn memcmp_le(b1: *const u8, b2: *const u8, len: usize) -> i32 {
|
||||
let mut res = 0;
|
||||
for i in 0..len {
|
||||
let diff =
|
||||
i32::from(ptr::read_volatile(b1.add(i))) - i32::from(ptr::read_volatile(b2.add(i)));
|
||||
res = (res & (((diff - 1) & !diff) >> 8)) | diff;
|
||||
}
|
||||
((res - 1) >> 8) + (res >> 8) + 1
|
||||
}
|
||||
|
||||
/// compares two slices of memory content and returns an integer indicating the relationship between
|
||||
/// the slices
|
||||
///
|
||||
/// ## Returns
|
||||
/// - <0 if the first byte that does not match both slices has a lower value in `a` than in `b`
|
||||
/// - 0 if the contents are equal
|
||||
/// - >0 if the first byte that does not match both slices has a higher value in `a` than in `b`
|
||||
///
|
||||
/// ## Leaks
|
||||
/// If the two slices have differents lengths, the function will return immediately. This
|
||||
/// effectively leaks the information whether the slices have equal length or not. This is widely
|
||||
/// considered safe.
|
||||
///
|
||||
/// The execution time of the function grows approx. linear with the length of the input. This is
|
||||
/// considered safe.
|
||||
///
|
||||
/// ## Tests
|
||||
/// For discussion on how to ensure the constant-time execution of this function, see
|
||||
/// <https://github.com/rosenpass/rosenpass/issues/232>
|
||||
#[inline]
|
||||
pub fn compare(a: &[u8], b: &[u8]) -> i32 {
|
||||
assert!(a.len() == b.len());
|
||||
unsafe { memcmp_le(a.as_ptr(), b.as_ptr(), a.len()) }
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
use core::hint::black_box;
|
||||
|
||||
/// Interpret the given slice as a little-endian unsigned integer
|
||||
/// and increment that integer.
|
||||
///
|
||||
/// # Leaks
|
||||
/// TODO: mention here if this function leaks any information, see
|
||||
/// <https://github.com/rosenpass/rosenpass/issues/232>
|
||||
///
|
||||
/// ## Tests
|
||||
/// For discussion on how to ensure the constant-time execution of this function, see
|
||||
/// <https://github.com/rosenpass/rosenpass/issues/232>
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_constant_time::increment as inc;
|
||||
/// use rosenpass_to::To;
|
||||
///
|
||||
/// fn testcase(v: &[u8], correct: &[u8]) {
|
||||
/// let mut v = v.to_owned();
|
||||
/// inc(&mut v);
|
||||
/// assert_eq!(&v, correct);
|
||||
/// }
|
||||
///
|
||||
/// testcase(b"", b"");
|
||||
/// testcase(b"\x00", b"\x01");
|
||||
/// testcase(b"\x01", b"\x02");
|
||||
/// testcase(b"\xfe", b"\xff");
|
||||
/// testcase(b"\xff", b"\x00");
|
||||
/// testcase(b"\x00\x00", b"\x01\x00");
|
||||
/// testcase(b"\x01\x00", b"\x02\x00");
|
||||
/// testcase(b"\xfe\x00", b"\xff\x00");
|
||||
/// testcase(b"\xff\x00", b"\x00\x01");
|
||||
/// testcase(b"\x00\x00\x00\x00\x00\x00", b"\x01\x00\x00\x00\x00\x00");
|
||||
/// testcase(b"\x00\xa3\x00\x77\x00\x00", b"\x01\xa3\x00\x77\x00\x00");
|
||||
/// testcase(b"\xff\xa3\x00\x77\x00\x00", b"\x00\xa4\x00\x77\x00\x00");
|
||||
/// testcase(b"\xff\xff\xff\x77\x00\x00", b"\x00\x00\x00\x78\x00\x00");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn increment(v: &mut [u8]) {
|
||||
let mut carry = 1u8;
|
||||
for val in v.iter_mut() {
|
||||
let (v, c) = black_box(*val).overflowing_add(black_box(carry));
|
||||
*black_box(val) = v;
|
||||
*black_box(&mut carry) = black_box(black_box(c) as u8);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,79 @@
|
||||
//! constant-time implementations of some primitives
|
||||
//!
|
||||
//! Rosenpass internal library providing basic constant-time operations.
|
||||
//!
|
||||
//! ## TODO
|
||||
//! Figure out methodology to ensure that code is actually constant time, see
|
||||
//! <https://github.com/rosenpass/rosenpass/issues/232>
|
||||
use core::hint::black_box;
|
||||
|
||||
mod compare;
|
||||
mod increment;
|
||||
mod memcmp;
|
||||
mod xor;
|
||||
use rosenpass_to::{with_destination, To};
|
||||
|
||||
pub use compare::compare;
|
||||
pub use increment::increment;
|
||||
pub use memcmp::memcmp;
|
||||
pub use xor::xor;
|
||||
/// Xors the source into the destination
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_constant_time::xor;
|
||||
/// use rosenpass_to::To;
|
||||
/// assert_eq!(
|
||||
/// xor(b"world").to_this(|| b"hello".to_vec()),
|
||||
/// b"\x1f\n\x1e\x00\x0b");
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If source and destination are of different sizes.
|
||||
#[inline]
|
||||
pub fn xor(src: &[u8]) -> impl To<[u8], ()> + '_ {
|
||||
with_destination(|dst: &mut [u8]| {
|
||||
assert!(black_box(src.len()) == black_box(dst.len()));
|
||||
for (dv, sv) in dst.iter_mut().zip(src.iter()) {
|
||||
*black_box(dv) ^= black_box(*sv);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn memcmp(a: &[u8], b: &[u8]) -> bool {
|
||||
a.len() == b.len()
|
||||
&& unsafe { memsec::memeq(a.as_ptr() as *const u8, b.as_ptr() as *const u8, a.len()) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn compare(a: &[u8], b: &[u8]) -> i32 {
|
||||
assert!(a.len() == b.len());
|
||||
unsafe { memsec::memcmp(a.as_ptr(), b.as_ptr(), a.len()) }
|
||||
}
|
||||
|
||||
/// Interpret the given slice as a little-endian unsigned integer
|
||||
/// and increment that integer.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_constant_time::increment as inc;
|
||||
/// use rosenpass_to::To;
|
||||
///
|
||||
/// fn testcase(v: &[u8], correct: &[u8]) {
|
||||
/// let mut v = v.to_owned();
|
||||
/// inc(&mut v);
|
||||
/// assert_eq!(&v, correct);
|
||||
/// }
|
||||
///
|
||||
/// testcase(b"", b"");
|
||||
/// testcase(b"\x00", b"\x01");
|
||||
/// testcase(b"\x01", b"\x02");
|
||||
/// testcase(b"\xfe", b"\xff");
|
||||
/// testcase(b"\xff", b"\x00");
|
||||
/// testcase(b"\x00\x00", b"\x01\x00");
|
||||
/// testcase(b"\x01\x00", b"\x02\x00");
|
||||
/// testcase(b"\xfe\x00", b"\xff\x00");
|
||||
/// testcase(b"\xff\x00", b"\x00\x01");
|
||||
/// testcase(b"\x00\x00\x00\x00\x00\x00", b"\x01\x00\x00\x00\x00\x00");
|
||||
/// testcase(b"\x00\xa3\x00\x77\x00\x00", b"\x01\xa3\x00\x77\x00\x00");
|
||||
/// testcase(b"\xff\xa3\x00\x77\x00\x00", b"\x00\xa4\x00\x77\x00\x00");
|
||||
/// testcase(b"\xff\xff\xff\x77\x00\x00", b"\x00\x00\x00\x78\x00\x00");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn increment(v: &mut [u8]) {
|
||||
let mut carry = 1u8;
|
||||
for val in v.iter_mut() {
|
||||
let (v, c) = black_box(*val).overflowing_add(black_box(carry));
|
||||
*black_box(val) = v;
|
||||
*black_box(&mut carry) = black_box(black_box(c) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
/// compares two sclices of memory content and returns whether they are equal
|
||||
///
|
||||
/// ## Leaks
|
||||
/// If the two slices have differents lengths, the function will return immediately. This
|
||||
/// effectively leaks the information whether the slices have equal length or not. This is widely
|
||||
/// considered safe.
|
||||
///
|
||||
/// The execution time of the function grows approx. linear with the length of the input. This is
|
||||
/// considered safe.
|
||||
///
|
||||
/// ## Tests
|
||||
/// [`tests::memcmp_runs_in_constant_time`] runs a stasticial test that the equality of the two
|
||||
/// input parameters does not correlate with the run time.
|
||||
///
|
||||
/// For discussion on how to (further) ensure the constant-time execution of this function,
|
||||
/// see <https://github.com/rosenpass/rosenpass/issues/232>
|
||||
#[inline]
|
||||
pub fn memcmp(a: &[u8], b: &[u8]) -> bool {
|
||||
a.len() == b.len() && unsafe { memsec::memeq(a.as_ptr(), b.as_ptr(), a.len()) }
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "constant_time_tests"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
/// tests whether [memcmp] actually runs in constant time
|
||||
///
|
||||
/// This test function will run an equal amount of comparisons on two different sets of parameters:
|
||||
/// - completely equal slices
|
||||
/// - completely unequal slices.
|
||||
/// All comparisons are executed in a randomized order. The test will fail if one of the
|
||||
/// two sets is checked for equality significantly faster than the other set
|
||||
/// (absolute correlation coefficient ≥ 0.01)
|
||||
fn memcmp_runs_in_constant_time() {
|
||||
// prepare data to compare
|
||||
let n: usize = 1E6 as usize; // number of comparisons to run
|
||||
let len = 1024; // length of each slice passed as parameters to the tested comparison function
|
||||
let a1 = "a".repeat(len);
|
||||
let a2 = a1.clone();
|
||||
let b = "b".repeat(len);
|
||||
|
||||
let a1 = a1.as_bytes();
|
||||
let a2 = a2.as_bytes();
|
||||
let b = b.as_bytes();
|
||||
|
||||
// vector representing all timing tests
|
||||
//
|
||||
// Each element is a tuple of:
|
||||
// 0: whether the test compared two equal slices
|
||||
// 1: the duration needed for the comparison to run
|
||||
let mut tests = (0..n)
|
||||
.map(|i| (i < n / 2, std::time::Duration::ZERO))
|
||||
.collect::<Vec<_>>();
|
||||
tests.shuffle(&mut thread_rng());
|
||||
|
||||
// run comparisons / call function to test
|
||||
for test in tests.iter_mut() {
|
||||
let now = Instant::now();
|
||||
if test.0 {
|
||||
memcmp(a1, a2);
|
||||
} else {
|
||||
memcmp(a1, b);
|
||||
}
|
||||
test.1 = now.elapsed();
|
||||
// println!("eq: {}, elapsed: {:.2?}", test.0, test.1);
|
||||
}
|
||||
|
||||
// sort by execution time and calculate Pearson correlation coefficient
|
||||
tests.sort_by_key(|v| v.1);
|
||||
let tests = tests
|
||||
.iter()
|
||||
.map(|t| (if t.0 { 1_f64 } else { 0_f64 }, t.1.as_nanos() as f64))
|
||||
.collect::<Vec<_>>();
|
||||
// averages
|
||||
let (avg_x, avg_y): (f64, f64) = (
|
||||
tests.iter().map(|t| t.0).sum::<f64>() / n as f64,
|
||||
tests.iter().map(|t| t.1).sum::<f64>() / n as f64,
|
||||
);
|
||||
assert!((avg_x - 0.5).abs() < 1E-12);
|
||||
// standard deviations
|
||||
let sd_x = 0.5;
|
||||
let sd_y = (1_f64 / n as f64
|
||||
* tests
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let difference = t.1 - avg_y;
|
||||
difference * difference
|
||||
})
|
||||
.sum::<f64>())
|
||||
.sqrt();
|
||||
// covariance
|
||||
let cv = 1_f64 / n as f64
|
||||
* tests
|
||||
.iter()
|
||||
.map(|t| (t.0 - avg_x) * (t.1 - avg_y))
|
||||
.sum::<f64>();
|
||||
// Pearson correlation
|
||||
let correlation = cv / (sd_x * sd_y);
|
||||
println!("correlation: {:.6?}", correlation);
|
||||
assert!(
|
||||
correlation.abs() < 0.01,
|
||||
"execution time correlates with result"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use core::hint::black_box;
|
||||
use rosenpass_to::{with_destination, To};
|
||||
|
||||
/// Xors the source into the destination
|
||||
///
|
||||
/// # Panics
|
||||
/// If source and destination are of different sizes.
|
||||
///
|
||||
/// # Leaks
|
||||
/// TODO: mention here if this function leaks any information, see
|
||||
/// <https://github.com/rosenpass/rosenpass/issues/232>
|
||||
///
|
||||
/// ## Tests
|
||||
/// For discussion on how to ensure the constant-time execution of this function, see
|
||||
/// <https://github.com/rosenpass/rosenpass/issues/232>
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_constant_time::xor;
|
||||
/// use rosenpass_to::To;
|
||||
/// assert_eq!(
|
||||
/// xor(b"world").to_this(|| b"hello".to_vec()),
|
||||
/// b"\x1f\n\x1e\x00\x0b");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn xor(src: &[u8]) -> impl To<[u8], ()> + '_ {
|
||||
with_destination(|dst: &mut [u8]| {
|
||||
assert!(black_box(src.len()) == black_box(dst.len()));
|
||||
for (dv, sv) in dst.iter_mut().zip(src.iter()) {
|
||||
*black_box(dv) ^= black_box(*sv);
|
||||
}
|
||||
})
|
||||
}
|
||||
13
doc/check.sh
13
doc/check.sh
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# We have to filter this STYLE error out, because it is very platform specific
|
||||
OUTPUT=$(mandoc -Tlint "$1" | grep --invert-match "STYLE: referenced manual not found")
|
||||
|
||||
if [ -z "$OUTPUT" ]
|
||||
then
|
||||
exit 0
|
||||
else
|
||||
echo "$1 is malformatted, check mandoc -Tlint $1"
|
||||
echo "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
109
doc/setup/multi_device_isolation.md
Normal file
109
doc/setup/multi_device_isolation.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Multi Device Isolation
|
||||
|
||||
On supported systems (just Linux, state Feb. 2024), Rosenpass uses a so-called broker architecture where multiple operating system processes work together to negotiate a key using post quantum cryptography and then send it to WireGuard to run an actual VPN. This is similar to the sandboxes used by web browsers to prevent websites accessing the rest of the computer.
|
||||
|
||||
These processes communicate using what is called a "unix socket"; a special file that can be used by to processes to send data back and forth. There are tools to forward data from a unix socket one one host to a unix socket on another host. By using one of these tools, you can run most of Rosenpass' processes on one host while running just the process that forwards keys from Rosenpass to WireGuard on the device that establishes the WireGuard tunnel.
|
||||
|
||||
This type of setup can provide a very high degree of isolation. When set up correctly, a critical bug in the Rosenpass code can not affect the host running WireGuard and vice versa. Keep in mind though that for this goal to be reached, the method to connect both hosts must be sufficiently secured: If the WireGuard host is allowed to perform arbitrary commands on the Rosenpass host, then an attacker with access to the WireGuard device can also take over the Rosenpass device.
|
||||
|
||||
You can use the instructions from [Unix Domain Socket Forwarding with OpenSSH](https://www.skreutz.com/posts/unix-domain-socket-forwarding-with-openssh/) to harden the connection between the two devices after following the instructions from this tutorial.
|
||||
|
||||
## Instructions
|
||||
|
||||
In this manual, we are dealing with three hosts:
|
||||
|
||||
- The **local peer**: The local host running WireGuard
|
||||
- The **remote peer**: The remote host we are connecting to.
|
||||
- The **the rosenpass device**: The dedicated host running rosenpass. It connects with *local peer* to supply WireGuard with keys and it connects with remote peer to perform key exchanges.
|
||||
|
||||
Lets assume, that you are starting from a working Rosenpass setup on *local peer* and *remote peer*, running rosenpass and WireGuard on each host. Both setups use configuration files. We will move the rosenpass instance running on *local peer* to the *rosenpass device* in this tutorial.
|
||||
|
||||
### Step 0: Setup Rosenpass
|
||||
|
||||
If you do not have a functioning rosenpass deployment on the local and remote peer at this point, you can create one by using the configuration files from the configuration-examples directory.
|
||||
|
||||
You will need to set up the WireGuard device manually using instructions for your linux distribution. You can use the tutorial on the [arch wiki](https://wiki.archlinux.org/title/WireGuard) for reference.
|
||||
|
||||
Make sure to set a random pre-shared key during creation of the WireGuard setup on both hosts at startup. Random pre-shared keys can be generated by using `wg genpks` on each host.
|
||||
|
||||
For the broker based setup to work, you might have to assign the broker process the CAP_NET_ADMIN linux capability:
|
||||
|
||||
```bash
|
||||
sudo setcap CAP_NET_ADMIN=+eip ./target/debug/rosenpass-wireguard-broker-privileged
|
||||
```
|
||||
|
||||
Make sure the broker binaries are in your system path when starting rosenpass:
|
||||
|
||||
```bash
|
||||
PATH="$PWD/target/debug:$PATH" ./target/debug/rosenpass exchange-config ./path/to/config/file.toml
|
||||
```
|
||||
|
||||
You will also need to setup rosenpass on the rosenpass device.
|
||||
|
||||
### Step 1: Verify that your rosenpass setup is working
|
||||
|
||||
Start rosenpass on both peers using the following command.
|
||||
|
||||
```bash
|
||||
PATH="$PWD/target/debug:$PATH" rosenpass exchange-config ./path/to/config/file.toml
|
||||
```
|
||||
|
||||
Now you can verify that rosenpass inserted a pre-shared key on both hosts:
|
||||
|
||||
```bash
|
||||
wg show wgRpTest preshared-keys
|
||||
```
|
||||
|
||||
The shell output will look similar to this:
|
||||
|
||||
```
|
||||
tdnV/wa/0Uf8Nrm3cZkKXOm4atrOEPzv1+dvaG7p7y0= 5235LJ/ONgrO8XuxECtLPzGOyWSvuzHcexzcgoHubfs=
|
||||
```
|
||||
|
||||
The first value is the peer's public key, the second in the pre-shared key. The pre-shared keys should match.
|
||||
|
||||
### Step 2: Manually start the broker
|
||||
|
||||
Rosenpass starts the psk-broker internally by default. We are looking to manually start it instead.
|
||||
|
||||
On the *local peer*, first start the broker manually:
|
||||
|
||||
```bash
|
||||
rm -fv broker.sock; PATH="target/debug" ./target/debug/rosenpass-wireguard-broker-socket-handler --listen-path broker.sock
|
||||
```
|
||||
|
||||
Now you should call rosenpass while make use of the created socket. Use the `psk_broker` configuration key. Your configuration will now look something like this:
|
||||
|
||||
```bash
|
||||
PATH="$PWD/target/debug:$PATH" rosenpass --psk-broker broker.sock exchange-config ./path/to/config/file.toml
|
||||
```
|
||||
|
||||
### Step 2: Forward the unix socket to the rosenpass device
|
||||
|
||||
OpenSSH socket forwarding can be used; on the local peer you can execute something like the following command:
|
||||
|
||||
```bash
|
||||
ssh -vgMR path/to/rosenpass/broker.sock:./broker.sock -L user@rosenpass_device
|
||||
```
|
||||
|
||||
### Step 3: Start rosenpass on the rosenpass device
|
||||
|
||||
You may need to copy your configuration files to the rosenpass device:
|
||||
|
||||
```bash
|
||||
scp ./path/to/config/file.toml ./path/to/peer/pk ./path/to/pk ./path/to/sk user@rosenpass_device:path/to/rosenpass/
|
||||
```
|
||||
|
||||
Now you can start rosenpass on the rosenpass device:
|
||||
|
||||
```bash
|
||||
PATH="$PWD/target/debug:$PATH" ./target/debug/rosenpass exchange-config ./path/to/config.toml
|
||||
```
|
||||
|
||||
### Step 4: Harden the setup
|
||||
|
||||
This tutorial is in a very rough state; it currently provides enough hints to advanced users to convey how the setup is supposed work. For a real production setup it needs to be adapted.
|
||||
|
||||
In particular, you can use the guide from from [Unix Domain Socket Forwarding with OpenSSH](https://www.skreutz.com/posts/unix-domain-socket-forwarding-with-openssh/) to make sure neither the *local peer* nor the *rosenpass device* can execute arbitrary commands on each other. The socat tutorial used in this setup can be used to achieve a diversity of setups, such as forwarding the unix socket via a plain TCP socket without encryption to the rosenpass device, if a trusted network setup is used to connect the two. Other setups such as securing the connection using TLS or forwarding the connection via a serial connection can be achieved.
|
||||
|
||||
You should also make sure that the rosenpass secret key is at no point in time stored in the *local peer*, so if you followed this tutorial you might want to regenerate the keypair on the *rosenpass device* itself.
|
||||
9
examples/broker-in-podman-container/config/config.toml
Normal file
9
examples/broker-in-podman-container/config/config.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
public_key = "./pk1"
|
||||
secret_key = "./sk1"
|
||||
listen = ["[::]:9999"]
|
||||
verbosity = "Verbose"
|
||||
|
||||
[[peers]]
|
||||
public_key = "./pk2"
|
||||
device = "YOUR WIREGUARD DEVICE"
|
||||
peer = "YOUR PEER PK"
|
||||
@@ -0,0 +1,26 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
rosenpass:
|
||||
build: ../rosenpass/
|
||||
command: rosenpass
|
||||
ports:
|
||||
- name: rosenpass-ipv4
|
||||
target: 9999
|
||||
host_ip: 127.0.0.1
|
||||
published: 9999
|
||||
protocol: udp
|
||||
mode: host
|
||||
- name: rosenpass-ipv4
|
||||
target: 9999
|
||||
host_ip: '[::1]'
|
||||
published: 9999
|
||||
protocol: udp
|
||||
mode: host
|
||||
environment:
|
||||
RUST_LOG: info
|
||||
#DEBUG_ENTRYPOINT: 1
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
volumes:
|
||||
- ../socket:/socket:ro
|
||||
- ../config:/config:ro
|
||||
@@ -0,0 +1,14 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
rosenpass:
|
||||
build: ../rosenpass
|
||||
command: psk_broker
|
||||
environment:
|
||||
RUST_LOG: info
|
||||
USER_GAINS_CAP_NET_ADMIN: 1
|
||||
#DEBUG_ENTRYPOINT: 1
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
volumes:
|
||||
- ../socket:/socket:rw
|
||||
network_mode: host
|
||||
35
examples/broker-in-podman-container/rosenpass/Dockerfile
Normal file
35
examples/broker-in-podman-container/rosenpass/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
FROM rust:slim as build
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
libclang-dev \
|
||||
libcap2-bin \
|
||||
git \
|
||||
inotify-tools
|
||||
|
||||
RUN adduser --system --uid 768 --group rosenpass --home /var/lib/rosenpass
|
||||
|
||||
USER rosenpass:rosenpass
|
||||
RUN cd ~rosenpass \
|
||||
&& git clone --depth 1 "https://github.com/rosenpass/rosenpass" -b dev/broker-architecture rosenpass-source \
|
||||
&& cd rosenpass-source \
|
||||
&& mkdir -p ~rosenpass/usr \
|
||||
&& cargo install --path rosenpass --root ~rosenpass/usr --bins \
|
||||
&& cargo install --path wireguard-broker --root ~rosenpass/usr --bins \
|
||||
&& cd ~rosenpass \
|
||||
&& rm -R rosenpass-source
|
||||
USER root:root
|
||||
|
||||
# TODO: Is this proper handling of ambient capabilities?
|
||||
RUN setcap CAP_NET_ADMIN=+ep ~rosenpass/usr/bin/rosenpass-wireguard-broker-privileged
|
||||
|
||||
VOLUME /config
|
||||
COPY ./entrypoint.sh /usr/local/sbin/docker_entrypoint
|
||||
COPY ./fd_passing.pl /usr/local/sbin/fd_passing
|
||||
RUN chmod a+x /usr/local/sbin/docker_entrypoint /usr/local/sbin/fd_passing
|
||||
ENTRYPOINT ["/usr/local/sbin/docker_entrypoint"]
|
||||
|
||||
VOLUME /socket
|
||||
|
||||
CMD rosenpass config.toml
|
||||
187
examples/broker-in-podman-container/rosenpass/entrypoint.sh
Normal file
187
examples/broker-in-podman-container/rosenpass/entrypoint.sh
Normal file
@@ -0,0 +1,187 @@
|
||||
#! /bin/bash
|
||||
|
||||
set -e # Needed by bail()
|
||||
|
||||
log() {
|
||||
local lvl; lvl="${1}"; shift || bail "log()" "USAGE: log LEVEL CONTEXT MSG..."
|
||||
local ctx; ctx="${1}"; shift || bail "log()" "USAGE: log LEVEL CONTEXT MSG..."
|
||||
echo >&2 "[entrypoint.sh/${ctx} ${lvl}]:" "$@"
|
||||
}
|
||||
|
||||
log_debug() {
|
||||
if [[ -n "${DEBUG_ENTRYPOINT}" ]]; then
|
||||
log "DEBUG" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
log_info() {
|
||||
log "INFO" "$@"
|
||||
}
|
||||
|
||||
log_err() {
|
||||
log "ERROR" "$@"
|
||||
}
|
||||
|
||||
exc() {
|
||||
local ctx; ctx="${1}"; shift || bail "exc()" "USAGE: exc CONTEXT CMD..."
|
||||
log_debug '$' "$@"
|
||||
"$@"
|
||||
}
|
||||
|
||||
bail() {
|
||||
local ctx; ctx="${1}"; shift || bail "bail()" "USAGE: bail CONTEXT MSG..."
|
||||
(( "$#" != 0 )) || bail "${ctx}" $'USAGE: bail CONTEXT MSG... # Bail called without parameters! Please use error messages dear developer.'
|
||||
log_err "${ctx}" "$@"
|
||||
return 1
|
||||
}
|
||||
|
||||
join() {
|
||||
local delim; delim="$1"; shift || bail "join()" "USAGE: join DELIM ELMS..."
|
||||
local tmp fst
|
||||
fst="true"
|
||||
for tmp in "$@"; do
|
||||
if [[ "${fst}" = "true" ]]; then
|
||||
printf "%s" "${tmp}"
|
||||
fst=""
|
||||
else
|
||||
printf "%s%s" "${delim}" "${tmp}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Shred files after they where red (recursively)
|
||||
# USAGE: $ burn_after_reading DIR
|
||||
burn_after_reading() {
|
||||
local dir; dir="$1"; shift || bail "join()" "USAGE: burn_after_reading DIR"
|
||||
|
||||
log_info burn_after_reading "Started for ${dir}"
|
||||
|
||||
# Load the list of configuration files
|
||||
local -a files_arr # Array
|
||||
readarray -td $'\0' files_arr < <(find "${dir}" -type f -print0)
|
||||
|
||||
# Convert configuration file list to associative array
|
||||
local file
|
||||
local -A files_todo # Associative array
|
||||
for file in "${files_arr[@]}"; do
|
||||
files_todo["${file}"]="1"
|
||||
done
|
||||
|
||||
# Watch for closed files
|
||||
local file
|
||||
# The --exclude '/$' excludes directories
|
||||
inotifywait --quiet --monitor --event close_nowrite --exclude '/$' --recursive . --no-newline --format "%w%f%0" \
|
||||
| while read -d $'\0' -r file; do
|
||||
|
||||
# Check if the file is in the todo list, if yes, erase it
|
||||
if [[ "${files_todo["${file}"]+1}" = "1" ]]; then
|
||||
log_info burn_after_reading "File loaded from configuration; removing now: ${file}";
|
||||
shred "${file}"
|
||||
# Clear from the todo list; What in the devils name is this quoting style bash
|
||||
unset 'files_todo["${file}"]'
|
||||
fi
|
||||
|
||||
# We're done if the todo list is empty
|
||||
if (( "${#files_todo[@]}" == 0 )); then
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
as_user() {
|
||||
local -a cmd_prefix
|
||||
if [[ "$1" = "--exec" ]]; then
|
||||
cmd_prefix=("exec")
|
||||
shift
|
||||
fi
|
||||
|
||||
local user; user="$1"; shift || bail "as_user()" "USAGE: as_user USER CMD..."
|
||||
(( "$#" > 0 )) || bail "as_user()" "USAGE: as_user USER CMD..."
|
||||
|
||||
if [[ -n "${USER_GAINS_CAP_NET_ADMIN}" ]]; then # TODO: Dirty to do this here; use --cap-net-admin or something?
|
||||
exc "as_user()" "${cmd_prefix[@]}" \
|
||||
capsh --caps="cap_net_admin+eip cap_setuid,cap_setgid+ep" --keep=1 \
|
||||
--user="${user}" --addamb=cap_net_admin -- -c 'exec "$@"' -- "$@"
|
||||
elif [[ "${user}" = "$(whoami)" ]]; then
|
||||
exc "as_user()" "${cmd_prefix[@]}" "$@"
|
||||
else
|
||||
exc "as_user()" "${cmd_prefix[@]}" runuser -u "${user}" -- "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
bail "USAGE: ${SCRIPT} rosenpass|psk_broker"
|
||||
}
|
||||
|
||||
cmd_internal() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
cmd_run_command() {
|
||||
exc "run_command()" as_user --exec "${SWITCH_USER}" "$@"
|
||||
}
|
||||
|
||||
cmd_psk_broker() {
|
||||
exc "psk_broker()" exec \
|
||||
fd_passing --listen /socket/psk_broker.sock \
|
||||
"$SCRIPT" internal as_user --exec "${SWITCH_USER}" \
|
||||
rosenpass-wireguard-broker-socket-handler --listen-fd
|
||||
}
|
||||
|
||||
rosenpass_start_with_socket_fd() {
|
||||
local fd; fd="$1"; shift || bail "rosenpass_start_with_socket_fd()" "USAGE: rosenpass_start_with_socket_fd PSK_BROKER_FD"
|
||||
exc "rosenpass_start_with_socket_fd()" exec \
|
||||
rosenpass --psk-broker-fd "$fd" exchange-config /config/config.toml
|
||||
}
|
||||
|
||||
cmd_rosenpass() {
|
||||
test -z "${USER_GAINS_CAP_NET_ADMIN}" || bail "rosenpass()" "USER_GAINS_CAP_NET_ADMIN should be unset. The rosenpass instance doing key exchanges should not have network admin privileges!"
|
||||
exc "psk_broker()" exec \
|
||||
fd_passing --connect /socket/psk_broker.sock \
|
||||
"$SCRIPT" internal as_user --exec "${SWITCH_USER}" \
|
||||
"$SCRIPT" internal rosenpass_start_with_socket_fd
|
||||
}
|
||||
|
||||
main() {
|
||||
local command; command="$1"; shift || usage
|
||||
case "${command}" in
|
||||
internal) cmd_internal "$@" ;;
|
||||
run_command) ;;
|
||||
psk_broker) ;;
|
||||
rosenpass) ;;
|
||||
*) usage;;
|
||||
esac
|
||||
|
||||
exc "main()" umask u=rw,og=
|
||||
exc "main()" cp -R "${CONFIG_MOUNT}" "${CONFIG_TMP}"
|
||||
exc "main()" chmod -R u+X "${CONFIG_TMP}"
|
||||
exc "main()" chown -R rosenpass:rosenpass "${CONFIG_TMP}"
|
||||
# TODO: How can we do this? We should probably use a dedicated config broker.
|
||||
#exc "main()" umount "${CONFIG_MOUNT}"
|
||||
exc "main()" cd "${CONFIG_TMP}"
|
||||
|
||||
if [[ -n "${BURN_AFTER_READING}" ]]; then
|
||||
( burn_after_reading /dev/shm/rosenpass-config )&
|
||||
fi
|
||||
|
||||
local -a path_cpy extra_path_cpy
|
||||
mapfile -td ':' path_cpy < <(echo -n "$PATH")
|
||||
mapfile -td ':' extra_path_cpy < <(echo -n "$EXTRA_PATH")
|
||||
PATH="$(join ":" "${extra_path_cpy[@]}" "${path_cpy[@]}")"
|
||||
export PATH
|
||||
|
||||
exc "main()" "cmd_${command}" "$@"
|
||||
}
|
||||
|
||||
SCRIPT="$0"
|
||||
|
||||
# Config
|
||||
CONFIG_MOUNT="${CONFIG_MOUNT:-/config}"
|
||||
CONFIG_TMP="${CONFIG_TMP:-/dev/shm/rosenpass-config}"
|
||||
BURN_AFTER_READING="${BURN_AFTER_READING:-true}"
|
||||
SWITCH_USER="${SWITCH_USER:-rosenpass}"
|
||||
#USER_GAINS_CAP_NET_ADMIN="${USER_GAINS_CAP_NET_ADMIN}"
|
||||
EXTRA_PATH="${EXTRA_PATH:-"$(eval echo ~rosenpass)/usr/bin"}"
|
||||
|
||||
main "$@"
|
||||
38
examples/broker-in-podman-container/rosenpass/fd_passing.pl
Executable file
38
examples/broker-in-podman-container/rosenpass/fd_passing.pl
Executable file
@@ -0,0 +1,38 @@
|
||||
#! /usr/bin/perl
|
||||
|
||||
use Fcntl;
|
||||
use IO::Socket::UNIX;
|
||||
|
||||
my $usage = "[$0] Usage: $0 SOCKETPATH [--connect|--listen] CMD...";
|
||||
|
||||
my $mode = shift or die($usage);
|
||||
my $sopath = shift or die($usage);
|
||||
|
||||
|
||||
my $listen;
|
||||
if ($mode eq "--listen") {
|
||||
$listen = 1;
|
||||
} elsif ($mode eq "--connect") {
|
||||
$listen = 0;
|
||||
} else {
|
||||
die($usage);
|
||||
}
|
||||
|
||||
my $socket;
|
||||
if ($listen == 1) {
|
||||
$socket = IO::Socket::UNIX->new(
|
||||
Type => SOCK_STREAM(),
|
||||
Local => $sopath,
|
||||
Listen => 1,
|
||||
) or die "[$0] Error listening on socket socket: $!";
|
||||
} else {
|
||||
$socket = IO::Socket::UNIX->new(
|
||||
Type => SOCK_STREAM(),
|
||||
Peer => $sopath,
|
||||
) or die "[$0] Error listening on socket socket: $!";
|
||||
}
|
||||
|
||||
my $fd_flags = $socket->fcntl(F_GETFD, 0) or die "[$0] fcntl F_GETFD: $!";
|
||||
$socket->fcntl(F_SETFD, $fd_flags & ~FD_CLOEXEC) or die "[$0] fcntl F_SETFD: $!";
|
||||
|
||||
exec(@ARGV, $socket->fileno); # pass it on the command line
|
||||
0
examples/broker-in-podman-container/socket/.gitignore
vendored
Normal file
0
examples/broker-in-podman-container/socket/.gitignore
vendored
Normal file
38
flake.lock
generated
38
flake.lock
generated
@@ -2,15 +2,17 @@
|
||||
"nodes": {
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": ["nixpkgs"],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712298178,
|
||||
"narHash": "sha256-590fpCPXYAkaAeBz/V91GX4/KGzPObdYtqsTWzT6AhI=",
|
||||
"lastModified": 1699770036,
|
||||
"narHash": "sha256-bZmI7ytPAYLpyFNgj5xirDkKuAniOkj1xHdv5aIJ5GM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "569b5b5781395da08e7064e825953c548c26af76",
|
||||
"rev": "81ab0b4f7ae9ebb57daa0edf119c4891806e4d3a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -24,11 +26,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -39,7 +41,9 @@
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": ["nixpkgs"]
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1698420672,
|
||||
@@ -57,18 +61,16 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1712168706,
|
||||
"narHash": "sha256-XP24tOobf6GGElMd0ux90FEBalUtw6NkBSVh/RlA6ik=",
|
||||
"lastModified": 1698846319,
|
||||
"narHash": "sha256-4jyW/dqFBVpWFnhl0nvP6EN4lP7/ZqPxYRjl6var0Oc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1487bdea619e4a7a53a4590c475deabb5a9d1bfb",
|
||||
"rev": "34bdaaf1f0b7fb6d9091472edc968ff10a8c2857",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
@@ -82,11 +84,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1712156296,
|
||||
"narHash": "sha256-St7ZQrkrr5lmQX9wC1ZJAFxL8W7alswnyZk9d1se3Us=",
|
||||
"lastModified": 1699715108,
|
||||
"narHash": "sha256-yPozsobJU55gj+szgo4Lpcg1lHvGQYAT6Y4MrC80mWE=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "8e581ac348e223488622f4d3003cb2bd412bf27e",
|
||||
"rev": "5fcf5289e726785d20d3aa4d13d90a43ed248e83",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
158
flake.nix
158
flake.nix
@@ -1,6 +1,5 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
# for quicker rust builds
|
||||
@@ -36,6 +35,24 @@
|
||||
# normal nixpkgs
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
|
||||
# TODO remove overlay once a fix for
|
||||
# https://github.com/NixOS/nixpkgs/issues/216904 got merged
|
||||
overlays = [
|
||||
(
|
||||
final: prev: {
|
||||
iproute2 = prev.iproute2.overrideAttrs (old:
|
||||
let
|
||||
isStatic = prev.stdenv.hostPlatform.isStatic;
|
||||
in
|
||||
{
|
||||
makeFlags = old.makeFlags ++ prev.lib.optional isStatic [
|
||||
"TC_CONFIG_NO_XT=y"
|
||||
];
|
||||
});
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
|
||||
# parsed Cargo.toml
|
||||
@@ -72,9 +89,17 @@
|
||||
result = pkgs.lib.sources.cleanSourceWith { inherit src filter; };
|
||||
};
|
||||
|
||||
# builds a bin path for all dependencies for the `rp` shellscript
|
||||
rpBinPath = p: with p; lib.makeBinPath [
|
||||
coreutils
|
||||
findutils
|
||||
gawk
|
||||
wireguard-tools
|
||||
];
|
||||
|
||||
# a function to generate a nix derivation for rosenpass against any
|
||||
# given set of nixpkgs
|
||||
rosenpassDerivation = p:
|
||||
rpDerivation = p:
|
||||
let
|
||||
# whether we want to build a statically linked binary
|
||||
isStatic = p.targetPlatform.isStatic;
|
||||
@@ -120,10 +145,12 @@
|
||||
p.stdenv.cc
|
||||
cmake # for oqs build in the oqs-sys crate
|
||||
mandoc # for the built-in manual
|
||||
makeWrapper # for the rp shellscript
|
||||
pkg-config # let libsodium-sys-stable find libsodium
|
||||
removeReferencesTo
|
||||
rustPlatform.bindgenHook # for C-bindings in the crypto libs
|
||||
];
|
||||
buildInputs = with p; [ bash ];
|
||||
buildInputs = with p; [ bash libsodium ];
|
||||
|
||||
override = x: {
|
||||
preBuild =
|
||||
@@ -150,111 +177,11 @@
|
||||
preBuild = (lib.optionalString isStatic ''
|
||||
NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -lc"
|
||||
'');
|
||||
};
|
||||
|
||||
# We want to build for a specific target...
|
||||
CARGO_BUILD_TARGET = target;
|
||||
|
||||
# ... which might require a non-default linker:
|
||||
"CARGO_TARGET_${shout target}_LINKER" =
|
||||
let
|
||||
inherit (p.stdenv) cc;
|
||||
in
|
||||
"${cc}/bin/${cc.targetPrefix}cc";
|
||||
|
||||
meta = with pkgs.lib;
|
||||
{
|
||||
inherit (cargoToml.package) description homepage;
|
||||
license = with licenses; [ mit asl20 ];
|
||||
maintainers = [ maintainers.wucke13 ];
|
||||
platforms = platforms.all;
|
||||
};
|
||||
} // (lib.mkIf isStatic {
|
||||
# otherwise pkg-config tries to link non-existent dynamic libs
|
||||
# documented here: https://docs.rs/pkg-config/latest/pkg_config/
|
||||
PKG_CONFIG_ALL_STATIC = true;
|
||||
|
||||
# tell rust to build everything statically linked
|
||||
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
||||
});
|
||||
# a function to generate a nix derivation for the rp helper against any
|
||||
# given set of nixpkgs
|
||||
rpDerivation = p:
|
||||
let
|
||||
# whether we want to build a statically linked binary
|
||||
isStatic = p.targetPlatform.isStatic;
|
||||
|
||||
# the rust target of `p`
|
||||
target = p.rust.toRustTargetSpec p.targetPlatform;
|
||||
|
||||
# convert a string to shout case
|
||||
shout = string: builtins.replaceStrings [ "-" ] [ "_" ] (pkgs.lib.toUpper string);
|
||||
|
||||
# suitable Rust toolchain
|
||||
toolchain = with inputs.fenix.packages.${system}; combine [
|
||||
stable.cargo
|
||||
stable.rustc
|
||||
targets.${target}.stable.rust-std
|
||||
];
|
||||
|
||||
# naersk with a custom toolchain
|
||||
naersk = pkgs.callPackage inputs.naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
|
||||
# used to trick the build.rs into believing that CMake was ran **again**
|
||||
fakecmake = pkgs.writeScriptBin "cmake" ''
|
||||
#! ${pkgs.stdenv.shell} -e
|
||||
true
|
||||
'';
|
||||
in
|
||||
naersk.buildPackage
|
||||
{
|
||||
# metadata and source
|
||||
name = cargoToml.package.name;
|
||||
version = cargoToml.package.version;
|
||||
inherit src;
|
||||
|
||||
cargoBuildOptions = x: x ++ [ "-p" "rp" ];
|
||||
cargoTestOptions = x: x ++ [ "-p" "rp" ];
|
||||
|
||||
doCheck = true;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
p.stdenv.cc
|
||||
cmake # for oqs build in the oqs-sys crate
|
||||
mandoc # for the built-in manual
|
||||
removeReferencesTo
|
||||
rustPlatform.bindgenHook # for C-bindings in the crypto libs
|
||||
];
|
||||
buildInputs = with p; [ bash ];
|
||||
|
||||
override = x: {
|
||||
preBuild =
|
||||
# nix defaults to building for aarch64 _without_ the armv8-a crypto
|
||||
# extensions, but liboqs depens on these
|
||||
(lib.optionalString (system == "aarch64-linux") ''
|
||||
NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -march=armv8-a+crypto"
|
||||
''
|
||||
);
|
||||
|
||||
# fortify is only compatible with dynamic linking
|
||||
hardeningDisable = lib.optional isStatic "fortify";
|
||||
};
|
||||
|
||||
overrideMain = x: {
|
||||
# CMake detects that it was served a _foreign_ target dir, and CMake
|
||||
# would be executed again upon the second build step of naersk.
|
||||
# By adding our specially optimized CMake version, we reduce the cost
|
||||
# of recompilation by 99 % while, while avoiding any CMake errors.
|
||||
nativeBuildInputs = [ (lib.hiPrio fakecmake) ] ++ x.nativeBuildInputs;
|
||||
|
||||
# make sure that libc is linked, under musl this is not the case per
|
||||
# default
|
||||
preBuild = (lib.optionalString isStatic ''
|
||||
NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -lc"
|
||||
'');
|
||||
preInstall = ''
|
||||
install -D ${./rp} $out/bin/rp
|
||||
wrapProgram $out/bin/rp --prefix PATH : "${ rpBinPath p }"
|
||||
'';
|
||||
};
|
||||
|
||||
# We want to build for a specific target...
|
||||
@@ -296,8 +223,7 @@
|
||||
rec {
|
||||
packages = rec {
|
||||
default = rosenpass;
|
||||
rosenpass = rosenpassDerivation pkgs;
|
||||
rp = rpDerivation pkgs;
|
||||
rosenpass = rpDerivation pkgs;
|
||||
rosenpass-oci-image = rosenpassOCI "rosenpass";
|
||||
|
||||
# derivation for the release
|
||||
@@ -308,10 +234,6 @@
|
||||
if pkgs.hostPlatform.isLinux then
|
||||
packages.rosenpass-static
|
||||
else packages.rosenpass;
|
||||
rp =
|
||||
if pkgs.hostPlatform.isLinux then
|
||||
packages.rp-static
|
||||
else packages.rp;
|
||||
oci-image =
|
||||
if pkgs.hostPlatform.isLinux then
|
||||
packages.rosenpass-static-oci-image
|
||||
@@ -320,15 +242,14 @@
|
||||
pkgs.runCommandNoCC "lace-result" { }
|
||||
''
|
||||
mkdir {bin,$out}
|
||||
tar -cvf $out/rosenpass-${system}-${version}.tar \
|
||||
-C ${package} bin/rosenpass \
|
||||
-C ${rp} bin/rp
|
||||
cp ${./.}/rp bin/
|
||||
tar -cvf $out/rosenpass-${system}-${version}.tar bin/rp \
|
||||
-C ${package} bin/rosenpass
|
||||
cp ${oci-image} \
|
||||
$out/rosenpass-oci-image-${system}-${version}.tar.gz
|
||||
'';
|
||||
} // (if pkgs.stdenv.isLinux then rec {
|
||||
rosenpass-static = rosenpassDerivation pkgs.pkgsStatic;
|
||||
rp-static = rpDerivation pkgs.pkgsStatic;
|
||||
rosenpass-static = rpDerivation pkgs.pkgsStatic;
|
||||
rosenpass-static-oci-image = rosenpassOCI "rosenpass-static";
|
||||
} else { });
|
||||
}
|
||||
@@ -415,7 +336,6 @@
|
||||
cargo-release
|
||||
clippy
|
||||
nodePackages.prettier
|
||||
nushell # for the .ci/gen-workflow-files.nu script
|
||||
rustfmt
|
||||
packages.proverif-patched
|
||||
];
|
||||
|
||||
@@ -48,37 +48,13 @@ test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_box_secret_alloc_malloc"
|
||||
path = "fuzz_targets/box_secret_alloc_malloc.rs"
|
||||
name = "fuzz_box_secret_alloc"
|
||||
path = "fuzz_targets/box_secret_alloc.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_vec_secret_alloc_malloc"
|
||||
path = "fuzz_targets/vec_secret_alloc_malloc.rs"
|
||||
name = "fuzz_vec_secret_alloc"
|
||||
path = "fuzz_targets/vec_secret_alloc.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_box_secret_alloc_memfdsec"
|
||||
path = "fuzz_targets/box_secret_alloc_memfdsec.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_vec_secret_alloc_memfdsec"
|
||||
path = "fuzz_targets/vec_secret_alloc_memfdsec.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_box_secret_alloc_memfdsec_mallocfb"
|
||||
path = "fuzz_targets/box_secret_alloc_memfdsec_mallocfb.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_vec_secret_alloc_memfdsec_mallocfb"
|
||||
path = "fuzz_targets/vec_secret_alloc_memfdsec_mallocfb.rs"
|
||||
test = false
|
||||
doc = false
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use rosenpass_secret_memory::alloc::secret_box;
|
||||
use rosenpass_secret_memory::policy::*;
|
||||
use std::sync::Once;
|
||||
|
||||
static ONCE: Once = Once::new();
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
ONCE.call_once(secret_policy_try_use_memfd_secrets);
|
||||
let _ = secret_box(data);
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use rosenpass_secret_memory::alloc::secret_box;
|
||||
use rosenpass_secret_memory::policy::*;
|
||||
use std::sync::Once;
|
||||
static ONCE: Once = Once::new();
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
ONCE.call_once(secret_policy_use_only_malloc_secrets);
|
||||
let _ = secret_box(data);
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use rosenpass_secret_memory::alloc::secret_box;
|
||||
use rosenpass_secret_memory::policy::*;
|
||||
use std::sync::Once;
|
||||
|
||||
static ONCE: Once = Once::new();
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
ONCE.call_once(secret_policy_use_only_memfd_secrets);
|
||||
let _ = secret_box(data);
|
||||
});
|
||||
@@ -4,17 +4,11 @@ extern crate rosenpass;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use rosenpass::protocol::CryptoServer;
|
||||
use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::StaticKem;
|
||||
use rosenpass_secret_memory::policy::*;
|
||||
use rosenpass_secret_memory::Secret;
|
||||
use std::sync::Once;
|
||||
|
||||
static ONCE: Once = Once::new();
|
||||
fuzz_target!(|rx_buf: &[u8]| {
|
||||
ONCE.call_once(secret_policy_use_only_malloc_secrets);
|
||||
let sk = Secret::from_slice(&[0; StaticKem::SK_LEN]);
|
||||
let pk = Secret::from_slice(&[0; StaticKem::PK_LEN]);
|
||||
let sk = Secret::from_slice(&[0; 13568]);
|
||||
let pk = Secret::from_slice(&[0; 524160]);
|
||||
|
||||
let mut cs = CryptoServer::new(sk, pk);
|
||||
let mut tx_buf = [0; 10240];
|
||||
|
||||
@@ -9,12 +9,12 @@ use rosenpass_ciphers::kem::EphemeralKem;
|
||||
|
||||
#[derive(arbitrary::Arbitrary, Debug)]
|
||||
pub struct Input {
|
||||
pub pk: [u8; EphemeralKem::PK_LEN],
|
||||
pub pk: [u8; 800],
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
let mut ciphertext = [0u8; EphemeralKem::CT_LEN];
|
||||
let mut shared_secret = [0u8; EphemeralKem::SK_LEN];
|
||||
let mut ciphertext = [0u8; 768];
|
||||
let mut shared_secret = [0u8; 32];
|
||||
|
||||
EphemeralKem::encaps(&mut shared_secret, &mut ciphertext, &input.pk).unwrap();
|
||||
});
|
||||
|
||||
@@ -7,8 +7,8 @@ use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::StaticKem;
|
||||
|
||||
fuzz_target!(|input: [u8; StaticKem::PK_LEN]| {
|
||||
let mut ciphertext = [0u8; StaticKem::CT_LEN];
|
||||
let mut shared_secret = [0u8; StaticKem::SHK_LEN];
|
||||
let mut ciphertext = [0u8; 188];
|
||||
let mut shared_secret = [0u8; 32];
|
||||
|
||||
// We expect errors while fuzzing therefore we do not check the result.
|
||||
let _ = StaticKem::encaps(&mut shared_secret, &mut ciphertext, &input);
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
#![no_main]
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use rosenpass_secret_memory::alloc::secret_vec;
|
||||
use rosenpass_secret_memory::policy::*;
|
||||
|
||||
static ONCE: Once = Once::new();
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
ONCE.call_once(secret_policy_try_use_memfd_secrets);
|
||||
let mut vec = secret_vec();
|
||||
vec.extend_from_slice(data);
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
#![no_main]
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use rosenpass_secret_memory::alloc::secret_vec;
|
||||
use rosenpass_secret_memory::policy::*;
|
||||
|
||||
static ONCE: Once = Once::new();
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
ONCE.call_once(secret_policy_use_only_malloc_secrets);
|
||||
let mut vec = secret_vec();
|
||||
vec.extend_from_slice(data);
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
#![no_main]
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use rosenpass_secret_memory::alloc::secret_vec;
|
||||
use rosenpass_secret_memory::policy::*;
|
||||
|
||||
static ONCE: Once = Once::new();
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
ONCE.call_once(secret_policy_use_only_memfd_secrets);
|
||||
let mut vec = secret_vec();
|
||||
vec.extend_from_slice(data);
|
||||
});
|
||||
16
lenses/Cargo.toml
Normal file
16
lenses/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "rosenpass-lenses"
|
||||
version = "0.1.0"
|
||||
authors = ["Karolin Varner <karo@cupdev.net>", "wucke13 <wucke13@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Rosenpass internal library for parsing binary data securely"
|
||||
homepage = "https://rosenpass.eu/"
|
||||
repository = "https://github.com/rosenpass/rosenpass"
|
||||
readme = "readme.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
paste = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
3
lenses/readme.md
Normal file
3
lenses/readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Rosenpass internal binary parsing library
|
||||
|
||||
This is an internal library; no guarantee is made about its API at this point in time.
|
||||
206
lenses/src/lib.rs
Normal file
206
lenses/src/lib.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use std::result::Result;
|
||||
|
||||
/// Common trait shared by all Lenses
|
||||
pub trait LenseView {
|
||||
const LEN: usize;
|
||||
}
|
||||
|
||||
/// Error during lense creation
|
||||
#[derive(thiserror::Error, Debug, Eq, PartialEq, Clone)]
|
||||
pub enum LenseError {
|
||||
#[error("buffer size mismatch")]
|
||||
BufferSizeMismatch,
|
||||
}
|
||||
|
||||
pub type LenseResult<T> = Result<T, LenseError>;
|
||||
|
||||
impl LenseError {
|
||||
pub fn ensure_exact_buffer_size(len: usize, required: usize) -> LenseResult<()> {
|
||||
(len == required)
|
||||
.then_some(())
|
||||
.ok_or(LenseError::BufferSizeMismatch)
|
||||
}
|
||||
|
||||
pub fn ensure_sufficient_buffer_size(len: usize, required: usize) -> LenseResult<()> {
|
||||
(len >= required)
|
||||
.then_some(())
|
||||
.ok_or(LenseError::BufferSizeMismatch)
|
||||
}
|
||||
}
|
||||
|
||||
/// A macro to create data lenses.
|
||||
#[macro_export]
|
||||
macro_rules! lense(
|
||||
// prefix @ offset ; optional meta ; field name : field length, ...
|
||||
(token_muncher_ref @ $offset:expr ; $( $attr:meta )* ; $field:ident : $len:expr $(, $( $tail:tt )+ )?) => {
|
||||
::paste::paste!{
|
||||
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
$( #[ $attr ] )*
|
||||
///
|
||||
#[doc = lense!(maybe_docstring_link $len)]
|
||||
/// bytes long
|
||||
pub fn $field(&self) -> &__ContainerType::Output {
|
||||
&self.0[$offset .. $offset + $len]
|
||||
}
|
||||
|
||||
/// The bytes until the
|
||||
#[doc = lense!(maybe_docstring_link Self::$field)]
|
||||
/// field
|
||||
pub fn [< until_ $field >](&self) -> &__ContainerType::Output {
|
||||
&self.0[0 .. $offset]
|
||||
}
|
||||
|
||||
// if the tail exits, consume it as well
|
||||
$(
|
||||
lense!{token_muncher_ref @ $offset + $len ; $( $tail )+ }
|
||||
)?
|
||||
}
|
||||
};
|
||||
|
||||
// prefix @ offset ; optional meta ; field name : field length, ...
|
||||
(token_muncher_mut @ $offset:expr ; $( $attr:meta )* ; $field:ident : $len:expr $(, $( $tail:tt )+ )?) => {
|
||||
::paste::paste!{
|
||||
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
$( #[ $attr ] )*
|
||||
///
|
||||
#[doc = lense!(maybe_docstring_link $len)]
|
||||
/// bytes long
|
||||
pub fn [< $field _mut >](&mut self) -> &mut __ContainerType::Output {
|
||||
&mut self.0[$offset .. $offset + $len]
|
||||
}
|
||||
|
||||
// if the tail exits, consume it as well
|
||||
$(
|
||||
lense!{token_muncher_mut @ $offset + $len ; $( $tail )+ }
|
||||
)?
|
||||
}
|
||||
};
|
||||
|
||||
// switch that yields literals unchanged, but creates docstring links to
|
||||
// constants
|
||||
// TODO the doc string link doesn't work if $x is taken from a generic,
|
||||
(maybe_docstring_link $x:literal) => (stringify!($x));
|
||||
(maybe_docstring_link $x:expr) => (stringify!([$x]));
|
||||
|
||||
// struct name < optional generics > := optional doc string field name : field length, ...
|
||||
($type:ident $( < $( $generic:ident ),+ > )? := $( $( #[ $attr:meta ] )* $field:ident : $len:expr ),+) => (::paste::paste!{
|
||||
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
/// A data lense to manipulate byte slices.
|
||||
///
|
||||
//// # Fields
|
||||
///
|
||||
$(
|
||||
/// - `
|
||||
#[doc = stringify!($field)]
|
||||
/// `:
|
||||
#[doc = lense!(maybe_docstring_link $len)]
|
||||
/// bytes
|
||||
)+
|
||||
pub struct $type<__ContainerType $(, $( $generic ),+ )? > (
|
||||
__ContainerType,
|
||||
// The phantom data is required, since all generics declared on a
|
||||
// type need to be used on the type.
|
||||
// https://doc.rust-lang.org/stable/error_codes/E0392.html
|
||||
$( $( ::core::marker::PhantomData<$generic> ),+ )?
|
||||
);
|
||||
|
||||
impl<__ContainerType $(, $( $generic: LenseView ),+ )? > $type<__ContainerType $(, $( $generic ),+ )? >{
|
||||
$(
|
||||
/// Size in bytes of the field `
|
||||
#[doc = !($field)]
|
||||
/// `
|
||||
pub const fn [< $field _len >]() -> usize{
|
||||
$len
|
||||
}
|
||||
)+
|
||||
|
||||
/// Verify that `len` exactly holds [Self]
|
||||
pub fn check_size(len: usize) -> ::rosenpass_lenses::LenseResult<()> {
|
||||
::rosenpass_lenses::LenseError::ensure_exact_buffer_size(len, $( $len + )+ 0)
|
||||
}
|
||||
}
|
||||
|
||||
// read-only accessor functions
|
||||
impl<'a, __ContainerType $(, $( $generic: LenseView ),+ )?> $type<&'a __ContainerType $(, $( $generic ),+ )?>
|
||||
where
|
||||
__ContainerType: std::ops::Index<std::ops::Range<usize>> + ?Sized,
|
||||
{
|
||||
lense!{token_muncher_ref @ 0 ; $( $( $attr )* ; $field : $len ),+ }
|
||||
|
||||
/// View into all bytes belonging to this Lense
|
||||
pub fn all_bytes(&self) -> &__ContainerType::Output {
|
||||
&self.0[0..Self::LEN]
|
||||
}
|
||||
}
|
||||
|
||||
// mutable accessor functions
|
||||
impl<'a, __ContainerType $(, $( $generic: LenseView ),+ )?> $type<&'a mut __ContainerType $(, $( $generic ),+ )?>
|
||||
where
|
||||
__ContainerType: std::ops::IndexMut<std::ops::Range<usize>> + ?Sized,
|
||||
{
|
||||
lense!{token_muncher_ref @ 0 ; $( $( $attr )* ; $field : $len ),+ }
|
||||
lense!{token_muncher_mut @ 0 ; $( $( $attr )* ; $field : $len ),+ }
|
||||
|
||||
/// View into all bytes belonging to this Lense
|
||||
pub fn all_bytes(&self) -> &__ContainerType::Output {
|
||||
&self.0[0..Self::LEN]
|
||||
}
|
||||
|
||||
/// View into all bytes belonging to this Lense
|
||||
pub fn all_bytes_mut(&mut self) -> &mut __ContainerType::Output {
|
||||
&mut self.0[0..Self::LEN]
|
||||
}
|
||||
}
|
||||
|
||||
// lense trait, allowing us to know the implementing lenses size
|
||||
impl<__ContainerType $(, $( $generic: LenseView ),+ )? > LenseView for $type<__ContainerType $(, $( $generic ),+ )? >{
|
||||
/// Number of bytes required to store this type in binary format
|
||||
const LEN: usize = $( $len + )+ 0;
|
||||
}
|
||||
|
||||
/// Extension trait to allow checked creation of a lense over
|
||||
/// some byte slice that contains a
|
||||
#[doc = lense!(maybe_docstring_link $type)]
|
||||
pub trait [< $type Ext >] {
|
||||
type __ContainerType;
|
||||
|
||||
/// Create a lense to the byte slice
|
||||
fn [< $type:snake >] $(< $($generic : LenseView),* >)? (self) -> ::rosenpass_lenses::LenseResult< $type<Self::__ContainerType, $( $($generic),+ )? >>;
|
||||
|
||||
/// Create a lense to the byte slice, automatically truncating oversized buffers
|
||||
fn [< $type:snake _ truncating >] $(< $($generic : LenseView),* >)? (self) -> ::rosenpass_lenses::LenseResult< $type<Self::__ContainerType, $( $($generic),+ )? >>;
|
||||
}
|
||||
|
||||
impl<'a> [< $type Ext >] for &'a [u8] {
|
||||
type __ContainerType = &'a [u8];
|
||||
|
||||
fn [< $type:snake >] $(< $($generic : LenseView),* >)? (self) -> ::rosenpass_lenses::LenseResult< $type<Self::__ContainerType, $( $($generic),+ )? >> {
|
||||
$type::<Self::__ContainerType, $( $($generic),+ )? >::check_size(self.len())?;
|
||||
Ok($type ( self, $( $( ::core::marker::PhantomData::<$generic> ),+ )? ))
|
||||
}
|
||||
|
||||
fn [< $type:snake _ truncating >] $(< $($generic : LenseView),* >)? (self) -> ::rosenpass_lenses::LenseResult< $type<Self::__ContainerType, $( $($generic),+ )? >> {
|
||||
let required_size = $( $len + )+ 0;
|
||||
::rosenpass_lenses::LenseError::ensure_sufficient_buffer_size(self.len(), required_size)?;
|
||||
[< $type Ext >]::[< $type:snake >](&self[..required_size])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> [< $type Ext >] for &'a mut [u8] {
|
||||
type __ContainerType = &'a mut [u8];
|
||||
fn [< $type:snake >] $(< $($generic : LenseView),* >)? (self) -> ::rosenpass_lenses::LenseResult< $type<Self::__ContainerType, $( $($generic),+ )? >> {
|
||||
$type::<Self::__ContainerType, $( $($generic),+ )? >::check_size(self.len())?;
|
||||
Ok($type ( self, $( $( ::core::marker::PhantomData::<$generic> ),+ )? ))
|
||||
}
|
||||
|
||||
fn [< $type:snake _ truncating >] $(< $($generic : LenseView),* >)? (self) -> ::rosenpass_lenses::LenseResult< $type<Self::__ContainerType, $( $($generic),+ )? >> {
|
||||
let required_size = $( $len + )+ 0;
|
||||
::rosenpass_lenses::LenseError::ensure_sufficient_buffer_size(self.len(), required_size)?;
|
||||
[< $type Ext >]::[< $type:snake >](&mut self[..required_size])
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
@@ -1,40 +0,0 @@
|
||||
# Additional files
|
||||
|
||||
This folder contains additional files that are used in the project.
|
||||
|
||||
## `generate_configs.py`
|
||||
|
||||
The script is used to generate configuration files for a benchmark setup
|
||||
consisting of a device under testing (DUT) and automatic test equipment (ATE),
|
||||
basically a strong machine capable of running multiple Rosenpass instances at
|
||||
once.
|
||||
|
||||
At the top of the script multiple variables can be set to configure the DUT IP
|
||||
address and more. Once configured you may run `python3 generate_configs.py` to
|
||||
create the configuration files.
|
||||
|
||||
A new folder called `output/` is created containing the subfolder `dut/` and
|
||||
`ate/`. The former has to be copied on the DUT, ideally reproducible hardware
|
||||
like a Raspberry Pi, while the latter is copied to the ATE, i.e. a laptop.
|
||||
|
||||
### Running a benchmark
|
||||
|
||||
On the ATE a run script is required since multiple instances of `rosenpass` are
|
||||
started with different configurations in parallel. The scripts are named after
|
||||
the number of instances they start, e.g. `run-50.sh` starts 50 instances.
|
||||
|
||||
```shell
|
||||
# on the ATE aka laptop
|
||||
cd output/ate
|
||||
./run-10.sh
|
||||
```
|
||||
|
||||
On the DUT you start a single Rosenpass instance with the configuration matching
|
||||
the ATE number of peers.
|
||||
|
||||
```shell
|
||||
# on the DUT aka Raspberry Pi
|
||||
rosenpass exchange-config configs/dut-10.toml
|
||||
```
|
||||
|
||||
Use whatever measurement tool you like to monitor the DUT and ATE.
|
||||
@@ -1,105 +0,0 @@
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
|
||||
config = dict(
|
||||
peer_counts=[1, 5, 10, 50, 100, 500],
|
||||
peer_count_max=100,
|
||||
ate_ip="192.168.2.1",
|
||||
dut_ip="192.168.2.4",
|
||||
dut_port=9999,
|
||||
path_to_rosenpass_bin="/Users/user/src/rosenppass/rosenpass/target/debug/rosenpass",
|
||||
)
|
||||
|
||||
print(config)
|
||||
|
||||
output_dir = Path("output")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
template_dut = """
|
||||
public_key = "keys/dut-public-key"
|
||||
secret_key = "keys/dut-secret-key"
|
||||
listen = ["{dut_ip}:{dut_port}"]
|
||||
verbosity = "Quiet"
|
||||
"""
|
||||
template_dut_peer = """
|
||||
[[peers]] # ATE-{i}
|
||||
public_key = "keys/ate-{i}-public-key"
|
||||
endpoint = "{ate_ip}:{ate_port}"
|
||||
key_out = "out/key_out_{i}"
|
||||
"""
|
||||
|
||||
template_ate = """
|
||||
public_key = "keys/ate-{i}-public-key"
|
||||
secret_key = "keys/ate-{i}-secret-key"
|
||||
listen = ["{ate_ip}:{ate_port}"]
|
||||
verbosity = "Quiet"
|
||||
|
||||
[[peers]] # DUT
|
||||
public_key = "keys/dut-public-key"
|
||||
endpoint = "{dut_ip}:{dut_port}"
|
||||
key_out = "out/key_out_{i}"
|
||||
"""
|
||||
|
||||
(output_dir / "dut" / "keys").mkdir(exist_ok=True, parents=True)
|
||||
(output_dir / "dut" / "out").mkdir(exist_ok=True, parents=True)
|
||||
(output_dir / "dut" / "configs").mkdir(exist_ok=True, parents=True)
|
||||
(output_dir / "ate" / "keys").mkdir(exist_ok=True, parents=True)
|
||||
(output_dir / "ate" / "out").mkdir(exist_ok=True, parents=True)
|
||||
(output_dir / "ate" / "configs").mkdir(exist_ok=True, parents=True)
|
||||
|
||||
for peer_count in config["peer_counts"]:
|
||||
dut_config = template_dut.format(**config)
|
||||
for i in range(peer_count):
|
||||
dut_config += template_dut_peer.format(**config, i=i, ate_port=50000 + i)
|
||||
|
||||
(output_dir / "dut" / "configs" / f"dut-{peer_count}.toml").write_text(dut_config)
|
||||
|
||||
if not (output_dir / "dut" / "keys" / "dut-public-key").exists():
|
||||
print("Generate DUT keys")
|
||||
run(
|
||||
[
|
||||
config["path_to_rosenpass_bin"],
|
||||
"gen-keys",
|
||||
f"configs/dut-{peer_count}.toml",
|
||||
],
|
||||
cwd=output_dir / "dut",
|
||||
)
|
||||
else:
|
||||
print("DUT keys already exist")
|
||||
|
||||
# copy the DUT public key to the ATE
|
||||
(output_dir / "ate" / "keys" / "dut-public-key").write_bytes(
|
||||
(output_dir / "dut" / "keys" / "dut-public-key").read_bytes()
|
||||
)
|
||||
|
||||
ate_script = "(trap 'kill 0' SIGINT; \\\n"
|
||||
|
||||
for i in range(config["peer_count_max"]):
|
||||
(output_dir / "ate" / "configs" / f"ate-{i}.toml").write_text(
|
||||
template_ate.format(**config, i=i, ate_port=50000 + i)
|
||||
)
|
||||
|
||||
if not (output_dir / "ate" / "keys" / f"ate-{i}-public-key").exists():
|
||||
# generate ATE keys
|
||||
run(
|
||||
[config["path_to_rosenpass_bin"], "gen-keys", f"configs/ate-{i}.toml"],
|
||||
cwd=output_dir / "ate",
|
||||
)
|
||||
else:
|
||||
print(f"ATE-{i} keys already exist")
|
||||
|
||||
# copy the ATE public keys to the DUT
|
||||
(output_dir / "dut" / "keys" / f"ate-{i}-public-key").write_bytes(
|
||||
(output_dir / "ate" / "keys" / f"ate-{i}-public-key").read_bytes()
|
||||
)
|
||||
|
||||
ate_script += (
|
||||
f"{config['path_to_rosenpass_bin']} exchange-config configs/ate-{i}.toml & \\\n"
|
||||
)
|
||||
|
||||
if (i + 1) in config["peer_counts"]:
|
||||
write_script = ate_script
|
||||
write_script += "wait)"
|
||||
|
||||
(output_dir / "ate" / f"run-{i+1}.sh").write_text(write_script)
|
||||
@@ -6,7 +6,6 @@ author:
|
||||
- Benjamin Lipp = Max Planck Institute for Security and Privacy (MPI-SP)
|
||||
- Wanja Zaeske
|
||||
- Lisa Schmidt = {Scientific Illustrator – \\url{mullana.de}}
|
||||
- Prabhpreet Dua
|
||||
abstract: |
|
||||
Rosenpass is used to create post-quantum-secure VPNs. Rosenpass computes a shared key, WireGuard (WG) [@wg] uses the shared key to establish a secure connection. Rosenpass can also be used without WireGuard, deriving post-quantum-secure symmetric keys for another application. The Rosenpass protocol builds on “Post-quantum WireGuard” (PQWG) [@pqwg] and improves it by using a cookie mechanism to provide security against state disruption attacks.
|
||||
|
||||
@@ -219,7 +218,6 @@ The server needs to store the following variables:
|
||||
* `spkm`
|
||||
* `biscuit_key` – Randomly chosen key used to encrypt biscuits
|
||||
* `biscuit_ctr` – Retransmission protection for biscuits
|
||||
* `cookie_secret`- A randomized cookie secret to derive cookies sent to peer when under load. This secret changes every 120 seconds
|
||||
|
||||
Not mandated per se, but required in practice:
|
||||
|
||||
@@ -245,7 +243,6 @@ The initiator stores the following local state for each ongoing handshake:
|
||||
* `ck` – The chaining key
|
||||
* `eski` – The initiator's ephemeral secret key
|
||||
* `epki` – The initiator's ephemeral public key
|
||||
* `cookie_value`- Cookie value sent by an initiator peer under load, used to compute cookie field in outgoing handshake to peer under load. This value expires 120 seconds from when a peer sends this value using the CookieReply message
|
||||
|
||||
The responder stores no state. While the responder has access to all of the above variables except for `eski`, the responder discards them after generating the RespHello message. Instead, the responder state is contained inside a cookie called a biscuit. This value is returned to the responder inside the InitConf packet. The biscuit consists of:
|
||||
|
||||
@@ -431,92 +428,12 @@ The responder code handling InitConf needs to deal with the biscuits and package
|
||||
|
||||
ICR5 and ICR6 perform biscuit replay protection using the biscuit number. This is not handled in `load_biscuit()` itself because there is the case that `biscuit_no = biscuit_used` which needs to be dealt with for retransmission handling.
|
||||
|
||||
### Denial of Service Mitigation and Cookies
|
||||
|
||||
Rosenpass derives its cookie-based DoS mitigation technique for a responder when receiving InitHello messages from Wireguard [@wg].
|
||||
|
||||
When the responder is under load, it may choose to not process further InitHello handshake messages, but instead to respond with a cookie reply message (see Figure \ref{img:MessageTypes}).
|
||||
|
||||
The sender of the exchange then uses this cookie in order to resend the message and have it accepted the following time by the reciever.
|
||||
|
||||
For an initiator, Rosenpass ignores all messages when under load.
|
||||
|
||||
#### Cookie Reply Message
|
||||
|
||||
The cookie reply message is sent by the responder on receiving an InitHello message when under load. It consists of the `sidi` of the initiator, a random 24-byte bitstring `nonce` and encrypting `cookie_value` into a `cookie_encrypted` reply field which consists of the following:
|
||||
|
||||
```pseudorust
|
||||
cookie_value = lhash("cookie-value", cookie_secret, initiator_host_info)[0..16]
|
||||
cookie_encrypted = XAEAD(lhash("cookie-key", spkm), nonce, cookie_value, mac_peer)
|
||||
```
|
||||
|
||||
where `cookie_secret` is a secret variable that changes every two minutes to a random value. `initiator_host_info` is used to identify the initiator host, and is implementation-specific for the client. This paramaters used to identify the host must be carefully chosen to ensure there is a unique mapping, especially when using IPv4 and IPv6 addresses to identify the host (such as taking care of IPv6 link-local addresses). `cookie_value` is a truncated 16 byte value from the above hash operation. `mac_peer` is the `mac` field of the peer's handshake message to which message is the reply.
|
||||
|
||||
#### Envelope `mac` Field
|
||||
|
||||
Similar to `mac.1` in Wireguard handshake messages, the `mac` field of a Rosenpass envelope from a handshake packet sender's point of view consists of the following:
|
||||
|
||||
```pseudorust
|
||||
mac = lhash("mac", spkt, MAC_WIRE_DATA)[0..16]
|
||||
```
|
||||
|
||||
where `MAC_WIRE_DATA` represents all bytes of msg prior to `mac` field in the envelope.
|
||||
|
||||
If a client receives an invalid `mac` value for any message, it will discard the message.
|
||||
|
||||
#### Envelope cookie field
|
||||
|
||||
The initiator, on receiving a CookieReply message, decrypts `cookie_encrypted` and stores the `cookie_value` for the session into `peer[sid].cookie_value` for a limited time (120 seconds). This value is then used to set `cookie` field set for subsequent messages and retransmissions to the responder as follows:
|
||||
|
||||
```pseudorust
|
||||
if (peer.cookie_value.is_none() || seconds_since_update(peer[sid].cookie_value) >= 120) {
|
||||
cookie.zeroize(); //zeroed out 16 bytes bitstring
|
||||
}
|
||||
else {
|
||||
cookie = lhash("cookie",peer.cookie_value.unwrap(),COOKIE_WIRE_DATA)
|
||||
}
|
||||
```
|
||||
|
||||
Here, `seconds_since_update(peer.cookie_value)` is the amount of time in seconds ellapsed since last cookie was received, and `COOKIE_WIRE_DATA` are the message contents of all bytes of the retransmitted message prior to the `cookie` field.
|
||||
|
||||
The inititator can use an invalid value for the `cookie` value, when the responder is not under load, and the responder must ignore this value.
|
||||
However, when the responder is under load, it may reject InitHello messages with the invalid `cookie` value, and issue a cookie reply message.
|
||||
|
||||
### Conditions to trigger DoS Mechanism
|
||||
|
||||
This whitepaper does not mandate any specific mechanism to detect responder contention (also mentioned as the under load condition) that would trigger use of the cookie mechanism.
|
||||
|
||||
For the reference implemenation, Rosenpass has derived inspiration from the linux implementation of Wireguard. This implementation suggests that the reciever keep track of the number of messages it is processing at a given time.
|
||||
|
||||
On receiving an incoming message, if the length of the message queue to be processed exceeds a threshold `MAX_QUEUED_INCOMING_HANDSHAKES_THRESHOLD`, the client is considered under load and its state is stored as under load. In addition, the timestamp of this instant when the client was last under load is stored. When recieving subsequent messages, if the client is still in an under load state, the client will check if the time ellpased since the client was last under load has exceeded `LAST_UNDER_LOAD_WINDOW` seconds. If this is the case, the client will update its state to normal operation, and process the message in a normal fashion.
|
||||
|
||||
Currently, the following constants are derived from the Linux kernel implementation of Wireguard:
|
||||
|
||||
```pseudorust
|
||||
MAX_QUEUED_INCOMING_HANDSHAKES_THRESHOLD = 4096
|
||||
LAST_UNDER_LOAD_WINDOW = 1 //seconds
|
||||
```
|
||||
|
||||
## Dealing with Packet Loss
|
||||
|
||||
The initiator deals with packet loss by storing the messages it sends to the responder and retransmitting them in randomized, exponentially increasing intervals until they get a response. Receiving RespHello terminates retransmission of InitHello. A Data or EmptyData message serves as acknowledgement of receiving InitConf and terminates its retransmission.
|
||||
|
||||
The responder does not need to do anything special to handle RespHello retransmission – if the RespHello package is lost, the initiator retransmits InitHello and the responder can generate another RespHello package from that. InitConf retransmission needs to be handled specifically in the responder code because accepting an InitConf retransmission would reset the live session including the nonce counter, which would cause nonce reuse. Implementations must detect the case that `biscuit_no = biscuit_used` in ICR5, skip execution of ICR6 and ICR7, and just transmit another EmptyData package to confirm that the initiator can stop transmitting InitConf.
|
||||
|
||||
### Interaction with cookie reply system
|
||||
|
||||
The cookie reply system does not interfere with the retransmission logic discussed above.
|
||||
|
||||
When the initator is under load, it will ignore processing any incoming messages.
|
||||
|
||||
When a responder is under load and it receives an InitHello handshake message, the InitHello message will be discarded and a cookie reply message is sent. The initiator, then on the reciept of the cookie reply message, will store a decrypted `cookie_value` to set the `cookie` field to subsequently sent messages. As per the retransmission mechanism above, the initiator will send a retransmitted InitHello message with a valid `cookie` value appended. On receiving the retransmitted handshake message, the responder will validate the `cookie` value and resume with the handshake process.
|
||||
|
||||
When the responder is under load and it recieves an InitConf message, the message will be directly processed without checking the validity of the cookie field.
|
||||
|
||||
# Changelog
|
||||
|
||||
- Added section "Denial of Service Mitigation and Cookies", and modify "Dealing with Packet Loss" for DoS cookie mechanism
|
||||
|
||||
\printbibliography
|
||||
|
||||
\setupimage{landscape,fullpage,label=img:HandlingCode}
|
||||
|
||||
@@ -25,11 +25,11 @@ Follow [quick start instructions](https://rosenpass.eu/#start) to get a VPN up a
|
||||
|
||||
## Software architecture
|
||||
|
||||
The [rosenpass tool](./src/) is written in Rust and uses liboqs[^liboqs]. The tool establishes a symmetric key and provides it to WireGuard. Since it supplies WireGuard with key through the PSK feature using Rosenpass+WireGuard is cryptographically no less secure than using WireGuard on its own ("hybrid security"). Rosenpass refreshes the symmetric key every two minutes.
|
||||
The [rosenpass tool](./src/) is written in Rust and uses liboqs[^liboqs] and libsodium[^libsodium]. The tool establishes a symmetric key and provides it to WireGuard. Since it supplies WireGuard with key through the PSK feature using Rosenpass+WireGuard is cryptographically no less secure than using WireGuard on its own ("hybrid security"). Rosenpass refreshes the symmetric key every two minutes.
|
||||
|
||||
As with any application a small risk of critical security issues (such as buffer overflows, remote code execution) exists; the Rosenpass application is written in the Rust programming language which is much less prone to such issues. Rosenpass can also write keys to files instead of supplying them to WireGuard With a bit of scripting the stand alone mode of the implementation can be used to run the application in a Container, VM or on another host. This mode can also be used to integrate tools other than WireGuard with Rosenpass.
|
||||
|
||||
The [`rp`](./rp) tool written in Rust makes it easy to create a VPN using WireGuard and Rosenpass.
|
||||
The [`rp`](./rp) tool written in bash makes it easy to create a VPN using WireGuard and Rosenpass.
|
||||
|
||||
`rp` is easy to get started with but has a few drawbacks; it runs as root, demanding access to both WireGuard
|
||||
and Rosenpass private keys, takes control of the interface and works with exactly one interface. If you do not feel confident about running Rosenpass as root, you should use the stand-alone mode to create a more secure setup using containers, jails, or virtual machines.
|
||||
@@ -59,6 +59,7 @@ The code uses a variety of optimizations to speed up analysis such as using secr
|
||||
A wrapper script provides instant feedback about which queries execute as expected in color: A red cross if a query fails and a green check if it succeeds.
|
||||
|
||||
[^liboqs]: https://openquantumsafe.org/liboqs/
|
||||
[^libsodium]: https://doc.libsodium.org/
|
||||
[^wg]: https://www.wireguard.com/
|
||||
[^pqwg]: https://eprint.iacr.org/2020/379
|
||||
[^pqwg-statedis]: Unless supplied with a pre-shared-key, but this defeats the purpose of a key exchange protocol
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
[package]
|
||||
name = "rosenpass"
|
||||
description = "Build post-quantum-secure VPNs with WireGuard!"
|
||||
version = "0.2.1"
|
||||
authors = ["Karolin Varner <karo@cupdev.net>", "wucke13 <wucke13@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Build post-quantum-secure VPNs with WireGuard!"
|
||||
homepage = "https://rosenpass.eu/"
|
||||
repository = "https://github.com/rosenpass/rosenpass"
|
||||
readme = "readme.md"
|
||||
|
||||
[[bin]]
|
||||
name = "rosenpass"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bench]]
|
||||
name = "handshake"
|
||||
harness = false
|
||||
@@ -24,6 +20,8 @@ rosenpass-ciphers = { workspace = true }
|
||||
rosenpass-cipher-traits = { workspace = true }
|
||||
rosenpass-to = { workspace = true }
|
||||
rosenpass-secret-memory = { workspace = true }
|
||||
rosenpass-lenses = { workspace = true }
|
||||
rosenpass-wireguard-broker = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
memoffset = { workspace = true }
|
||||
@@ -36,10 +34,8 @@ toml = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
mio = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
zerocopy = { workspace = true }
|
||||
home = { workspace = true }
|
||||
derive_builder = {workspace = true}
|
||||
rosenpass-wireguard-broker = {workspace = true}
|
||||
command-fds = { workspace = true }
|
||||
rustix = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
@@ -48,9 +44,3 @@ anyhow = { workspace = true }
|
||||
criterion = { workspace = true }
|
||||
test_bin = { workspace = true }
|
||||
stacker = { workspace = true }
|
||||
serial_test = {workspace = true}
|
||||
procspawn = {workspace = true}
|
||||
|
||||
[features]
|
||||
enable_broker_api = ["rosenpass-wireguard-broker/enable_broker_api"]
|
||||
enable_memfd_alloc = []
|
||||
@@ -5,7 +5,6 @@ use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::StaticKem;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use rosenpass_secret_memory::secret_policy_try_use_memfd_secrets;
|
||||
|
||||
fn handle(
|
||||
tx: &mut CryptoServer,
|
||||
@@ -57,7 +56,6 @@ fn make_server_pair() -> Result<(CryptoServer, CryptoServer)> {
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
secret_policy_try_use_memfd_secrets();
|
||||
let (mut a, mut b) = make_server_pair().unwrap();
|
||||
c.bench_function("cca_secret_alloc", |bench| {
|
||||
bench.iter(|| {
|
||||
|
||||
@@ -30,7 +30,8 @@ fn generate_man() -> String {
|
||||
return man;
|
||||
}
|
||||
|
||||
"Cannot render manual page. Please visit https://rosenpass.eu/docs/manuals/\n".into()
|
||||
// TODO: Link to online manual here
|
||||
"Cannot render manual page\n".into()
|
||||
}
|
||||
|
||||
fn man() {
|
||||
|
||||
@@ -1,52 +1,26 @@
|
||||
use anyhow::bail;
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_builder::Builder;
|
||||
use log::{error, info, warn};
|
||||
use mio::Interest;
|
||||
use mio::Token;
|
||||
use rosenpass_secret_memory::Public;
|
||||
use rosenpass_secret_memory::Secret;
|
||||
use rosenpass_util::file::StoreValueB64;
|
||||
use rosenpass_wireguard_broker::WireguardBrokerMio;
|
||||
use rosenpass_wireguard_broker::{WireguardBrokerCfg, WG_KEY_LEN};
|
||||
use zerocopy::AsBytes;
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::io::ErrorKind;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::Ipv6Addr;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::SocketAddrV4;
|
||||
use std::net::SocketAddrV6;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::slice;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::protocol::HostIdentification;
|
||||
use crate::{
|
||||
config::Verbosity,
|
||||
protocol::{CryptoServer, MsgBuf, PeerPtr, SPk, SSk, SymKey, Timing},
|
||||
};
|
||||
use rosenpass_util::attempt;
|
||||
use rosenpass_util::b64::B64Display;
|
||||
use anyhow::{bail, Result};
|
||||
use log::{error, info, warn};
|
||||
use mio::{Interest, Token};
|
||||
|
||||
const MAX_B64_KEY_SIZE: usize = 32 * 5 / 3;
|
||||
const MAX_B64_PEER_ID_SIZE: usize = 32 * 5 / 3;
|
||||
use rosenpass_secret_memory::Public;
|
||||
use rosenpass_util::b64::{b64_writer, fmt_b64};
|
||||
use rosenpass_util::{attempt, file::fopen_w};
|
||||
use rosenpass_wireguard_broker::api::mio_client::MioBrokerClient as PskBroker;
|
||||
use rosenpass_wireguard_broker::WireGuardBroker;
|
||||
|
||||
use crate::config::Verbosity;
|
||||
use crate::protocol::{CryptoServer, MsgBuf, PeerPtr, SPk, SSk, SymKey, Timing};
|
||||
|
||||
const IPV4_ANY_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
|
||||
const IPV6_ANY_ADDR: Ipv6Addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
const UNDER_LOAD_RATIO: f64 = 0.5;
|
||||
const DURATION_UPDATE_UNDER_LOAD_STATUS: Duration = Duration::from_millis(500);
|
||||
|
||||
const BROKER_ID_BYTES: usize = 8;
|
||||
|
||||
fn ipv4_any_binding() -> SocketAddr {
|
||||
// addr, port
|
||||
SocketAddr::V4(SocketAddrV4::new(IPV4_ANY_ADDR, 0))
|
||||
@@ -57,50 +31,23 @@ fn ipv6_any_binding() -> SocketAddr {
|
||||
SocketAddr::V6(SocketAddrV6::new(IPV6_ANY_ADDR, 0, 0, 0))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MioTokenDispenser {
|
||||
#[derive(Default)]
|
||||
struct MioTokenDispenser {
|
||||
counter: usize,
|
||||
}
|
||||
|
||||
impl MioTokenDispenser {
|
||||
fn dispense(&mut self) -> Token {
|
||||
fn get_token(&mut self) -> Token {
|
||||
let r = self.counter;
|
||||
self.counter += 1;
|
||||
Token(r)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BrokerStore {
|
||||
store: HashMap<
|
||||
Public<BROKER_ID_BYTES>,
|
||||
Box<dyn WireguardBrokerMio<Error = anyhow::Error, MioError = anyhow::Error>>,
|
||||
>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrokerStorePtr(pub Public<BROKER_ID_BYTES>);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BrokerPeer {
|
||||
ptr: BrokerStorePtr,
|
||||
peer_cfg: Box<dyn WireguardBrokerCfg>,
|
||||
}
|
||||
|
||||
impl BrokerPeer {
|
||||
pub fn new(ptr: BrokerStorePtr, peer_cfg: Box<dyn WireguardBrokerCfg>) -> Self {
|
||||
Self { ptr, peer_cfg }
|
||||
}
|
||||
|
||||
pub fn ptr(&self) -> &BrokerStorePtr {
|
||||
&self.ptr
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct AppPeer {
|
||||
pub outfile: Option<PathBuf>,
|
||||
pub broker_peer: Option<BrokerPeer>,
|
||||
pub outwg: Option<WireguardOut>, // TODO make this a generic command
|
||||
pub initial_endpoint: Option<Endpoint>,
|
||||
pub current_endpoint: Option<Endpoint>,
|
||||
}
|
||||
@@ -113,29 +60,22 @@ impl AppPeer {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub struct WireguardOut {
|
||||
// impl KeyOutput
|
||||
pub dev: String,
|
||||
pub pk: String,
|
||||
pub pk: Public<32>,
|
||||
pub extra_params: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum DoSOperation {
|
||||
UnderLoad,
|
||||
Normal,
|
||||
}
|
||||
/// Integration test helpers for AppServer
|
||||
#[derive(Debug, Builder)]
|
||||
#[builder(pattern = "owned")]
|
||||
pub struct AppServerTest {
|
||||
/// Enable DoS operation permanently
|
||||
#[builder(default = "false")]
|
||||
pub enable_dos_permanently: bool,
|
||||
/// Terminate application signal
|
||||
#[builder(default = "None")]
|
||||
pub termination_handler: Option<std::sync::mpsc::Receiver<()>>,
|
||||
impl Default for WireguardOut {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dev: Default::default(),
|
||||
pk: Public::zero(),
|
||||
extra_params: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the state of the application, namely the external IO
|
||||
@@ -148,17 +88,10 @@ pub struct AppServer {
|
||||
pub sockets: Vec<mio::net::UdpSocket>,
|
||||
pub events: mio::Events,
|
||||
pub mio_poll: mio::Poll,
|
||||
pub mio_token_dispenser: MioTokenDispenser,
|
||||
pub brokers: BrokerStore,
|
||||
pub psk_broker: RefCell<PskBroker>,
|
||||
pub peers: Vec<AppPeer>,
|
||||
pub verbosity: Verbosity,
|
||||
pub all_sockets_drained: bool,
|
||||
pub under_load: DoSOperation,
|
||||
pub blocking_polls_count: usize,
|
||||
pub non_blocking_polls_count: usize,
|
||||
pub unpolled_count: usize,
|
||||
pub last_update_time: Instant,
|
||||
pub test_helpers: Option<AppServerTest>,
|
||||
}
|
||||
|
||||
/// A socket pointer is an index assigned to a socket;
|
||||
@@ -207,17 +140,6 @@ impl AppPeerPtr {
|
||||
pub fn get_app_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut AppPeer {
|
||||
&mut srv.peers[self.0]
|
||||
}
|
||||
|
||||
pub fn set_psk(&self, server: &mut AppServer, psk: &Secret<WG_KEY_LEN>) -> anyhow::Result<()> {
|
||||
if let Some(broker) = server.peers[self.0].broker_peer.as_ref() {
|
||||
let config = broker.peer_cfg.create_config(psk);
|
||||
let broker = server.brokers.store.get_mut(&broker.ptr().0).unwrap();
|
||||
broker.set_psk(config)?;
|
||||
} else if server.peers[self.0].outfile.is_none() {
|
||||
log::warn!("No broker peer found for peer {}", self.0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -252,7 +174,13 @@ pub enum Endpoint {
|
||||
/// at the same time. It also would reply on the same port RespHello was
|
||||
/// sent to when listening on multiple ports on the same interface. This
|
||||
/// may be required for some arcane firewall setups.
|
||||
SocketBoundAddress(SocketBoundEndpoint),
|
||||
SocketBoundAddress {
|
||||
/// The socket the address can be reached under; this is generally
|
||||
/// determined when we actually receive an RespHello message
|
||||
socket: SocketPtr,
|
||||
/// Just the address
|
||||
addr: SocketAddr,
|
||||
},
|
||||
// A host name or IP address; storing the hostname here instead of an
|
||||
// ip address makes sure that we look up the host name whenever we try
|
||||
// to make a connection; this may be beneficial in some setups where a host-name
|
||||
@@ -260,85 +188,6 @@ pub enum Endpoint {
|
||||
Discovery(HostPathDiscoveryEndpoint),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Endpoint {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Endpoint::SocketBoundAddress(host) => write!(f, "{}", host),
|
||||
Endpoint::Discovery(host) => write!(f, "{}", host),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SocketBoundEndpoint {
|
||||
/// The socket the address can be reached under; this is generally
|
||||
/// determined when we actually receive an RespHello message
|
||||
socket: SocketPtr,
|
||||
/// Just the address
|
||||
addr: SocketAddr,
|
||||
/// identifier
|
||||
bytes: (usize, [u8; SocketBoundEndpoint::BUFFER_SIZE]),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SocketBoundEndpoint {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl SocketBoundEndpoint {
|
||||
const SOCKET_SIZE: usize = usize::BITS as usize / 8;
|
||||
const IPV6_SIZE: usize = 16;
|
||||
const PORT_SIZE: usize = 2;
|
||||
const SCOPE_ID_SIZE: usize = 4;
|
||||
|
||||
const BUFFER_SIZE: usize = SocketBoundEndpoint::SOCKET_SIZE
|
||||
+ SocketBoundEndpoint::IPV6_SIZE
|
||||
+ SocketBoundEndpoint::PORT_SIZE
|
||||
+ SocketBoundEndpoint::SCOPE_ID_SIZE;
|
||||
|
||||
pub fn new(socket: SocketPtr, addr: SocketAddr) -> Self {
|
||||
let bytes = Self::to_bytes(&socket, &addr);
|
||||
Self {
|
||||
socket,
|
||||
addr,
|
||||
bytes,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_bytes(
|
||||
socket: &SocketPtr,
|
||||
addr: &SocketAddr,
|
||||
) -> (usize, [u8; SocketBoundEndpoint::BUFFER_SIZE]) {
|
||||
let mut buf = [0u8; SocketBoundEndpoint::BUFFER_SIZE];
|
||||
let addr = match addr {
|
||||
SocketAddr::V4(addr) => {
|
||||
//Map IPv4-mapped to IPv6 addresses
|
||||
let ip = addr.ip().to_ipv6_mapped();
|
||||
SocketAddrV6::new(ip, addr.port(), 0, 0)
|
||||
}
|
||||
SocketAddr::V6(addr) => *addr,
|
||||
};
|
||||
let mut len: usize = 0;
|
||||
buf[len..len + SocketBoundEndpoint::SOCKET_SIZE].copy_from_slice(&socket.0.to_be_bytes());
|
||||
len += SocketBoundEndpoint::SOCKET_SIZE;
|
||||
buf[len..len + SocketBoundEndpoint::IPV6_SIZE].copy_from_slice(&addr.ip().octets());
|
||||
len += SocketBoundEndpoint::IPV6_SIZE;
|
||||
buf[len..len + SocketBoundEndpoint::PORT_SIZE].copy_from_slice(&addr.port().to_be_bytes());
|
||||
len += SocketBoundEndpoint::PORT_SIZE;
|
||||
buf[len..len + SocketBoundEndpoint::SCOPE_ID_SIZE]
|
||||
.copy_from_slice(&addr.scope_id().to_be_bytes());
|
||||
len += SocketBoundEndpoint::SCOPE_ID_SIZE;
|
||||
(len, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostIdentification for SocketBoundEndpoint {
|
||||
fn encode(&self) -> &[u8] {
|
||||
&self.bytes.1[0..self.bytes.0]
|
||||
}
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
/// Start discovery from some addresses
|
||||
pub fn discovery_from_addresses(addresses: Vec<SocketAddr>) -> Self {
|
||||
@@ -379,7 +228,7 @@ impl Endpoint {
|
||||
pub fn send(&self, srv: &AppServer, buf: &[u8]) -> anyhow::Result<()> {
|
||||
use Endpoint::*;
|
||||
match self {
|
||||
SocketBoundAddress(host) => host.socket.send_to(srv, buf, host.addr),
|
||||
SocketBoundAddress { socket, addr } => socket.send_to(srv, buf, *addr),
|
||||
Discovery(host) => host.send_scouting(srv, buf),
|
||||
}
|
||||
}
|
||||
@@ -387,7 +236,7 @@ impl Endpoint {
|
||||
fn addresses(&self) -> &[SocketAddr] {
|
||||
use Endpoint::*;
|
||||
match self {
|
||||
SocketBoundAddress(host) => slice::from_ref(&host.addr),
|
||||
SocketBoundAddress { addr, .. } => slice::from_ref(addr),
|
||||
Discovery(host) => host.addresses(),
|
||||
}
|
||||
}
|
||||
@@ -425,12 +274,6 @@ pub struct HostPathDiscoveryEndpoint {
|
||||
addresses: Vec<SocketAddr>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HostPathDiscoveryEndpoint {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self.addresses)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostPathDiscoveryEndpoint {
|
||||
pub fn from_addresses(addresses: Vec<SocketAddr>) -> Self {
|
||||
let scouting_state = Cell::new((0, 0));
|
||||
@@ -496,7 +339,7 @@ impl HostPathDiscoveryEndpoint {
|
||||
.to_string()
|
||||
.starts_with("Address family not supported by protocol");
|
||||
if !ignore {
|
||||
warn!("Socket #{} refusing to send to {}: {}", sock_no, addr, err);
|
||||
warn!("Socket #{} refusing to send to {}: ", sock_no, addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,13 +353,24 @@ impl AppServer {
|
||||
sk: SSk,
|
||||
pk: SPk,
|
||||
addrs: Vec<SocketAddr>,
|
||||
psk_broker_socket: UnixStream,
|
||||
verbosity: Verbosity,
|
||||
test_helpers: Option<AppServerTest>,
|
||||
) -> anyhow::Result<Self> {
|
||||
// setup mio
|
||||
let mio_poll = mio::Poll::new()?;
|
||||
let events = mio::Events::with_capacity(20);
|
||||
let mut mio_token_dispenser = MioTokenDispenser::default();
|
||||
let events = mio::Events::with_capacity(8);
|
||||
let mut dispenser = MioTokenDispenser::default();
|
||||
|
||||
// Create the Wireguard broker connection
|
||||
let psk_broker = {
|
||||
let mut sock = mio::net::UnixStream::from_std(psk_broker_socket);
|
||||
mio_poll.registry().register(
|
||||
&mut sock,
|
||||
dispenser.get_token(),
|
||||
Interest::READABLE | Interest::WRITABLE,
|
||||
)?;
|
||||
PskBroker::new(sock)
|
||||
};
|
||||
|
||||
// bind each SocketAddr to a socket
|
||||
let maybe_sockets: Result<Vec<_>, _> =
|
||||
@@ -590,12 +444,10 @@ impl AppServer {
|
||||
}
|
||||
|
||||
// register all sockets to mio
|
||||
for socket in sockets.iter_mut() {
|
||||
mio_poll.registry().register(
|
||||
socket,
|
||||
mio_token_dispenser.dispense(),
|
||||
Interest::READABLE,
|
||||
)?;
|
||||
for (i, socket) in sockets.iter_mut().enumerate() {
|
||||
mio_poll
|
||||
.registry()
|
||||
.register(socket, Token(i), Interest::READABLE)?;
|
||||
}
|
||||
|
||||
// TODO use mio::net::UnixStream together with std::os::unix::net::UnixStream for Linux
|
||||
@@ -603,19 +455,12 @@ impl AppServer {
|
||||
Ok(Self {
|
||||
crypt: CryptoServer::new(sk, pk),
|
||||
peers: Vec::new(),
|
||||
psk_broker: RefCell::new(psk_broker),
|
||||
verbosity,
|
||||
sockets,
|
||||
events,
|
||||
mio_poll,
|
||||
mio_token_dispenser,
|
||||
brokers: BrokerStore::default(),
|
||||
all_sockets_drained: false,
|
||||
under_load: DoSOperation::Normal,
|
||||
blocking_polls_count: 0,
|
||||
non_blocking_polls_count: 0,
|
||||
unpolled_count: 0,
|
||||
last_update_time: Instant::now(),
|
||||
test_helpers,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -623,50 +468,12 @@ impl AppServer {
|
||||
matches!(self.verbosity, Verbosity::Verbose)
|
||||
}
|
||||
|
||||
pub fn register_broker(
|
||||
&mut self,
|
||||
broker: Box<dyn WireguardBrokerMio<Error = anyhow::Error, MioError = anyhow::Error>>,
|
||||
) -> Result<BrokerStorePtr> {
|
||||
let ptr = Public::from_slice((self.brokers.store.len() as u64).as_bytes());
|
||||
|
||||
if self.brokers.store.insert(ptr, broker).is_some() {
|
||||
bail!("Broker already registered");
|
||||
}
|
||||
//Register broker
|
||||
self.brokers
|
||||
.store
|
||||
.get_mut(&ptr)
|
||||
.ok_or(anyhow::format_err!("Broker wasn't added to registry"))?
|
||||
.register(
|
||||
self.mio_poll.registry(),
|
||||
self.mio_token_dispenser.dispense(),
|
||||
)?;
|
||||
|
||||
Ok(BrokerStorePtr(ptr))
|
||||
}
|
||||
|
||||
pub fn unregister_broker(&mut self, ptr: BrokerStorePtr) -> Result<()> {
|
||||
//Unregister broker
|
||||
self.brokers
|
||||
.store
|
||||
.get_mut(&ptr.0)
|
||||
.ok_or_else(|| anyhow::anyhow!("Broker not found"))?
|
||||
.unregister(self.mio_poll.registry())?;
|
||||
|
||||
//Remove broker from store
|
||||
self.brokers
|
||||
.store
|
||||
.remove(&ptr.0)
|
||||
.ok_or_else(|| anyhow::anyhow!("Broker not found"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_peer(
|
||||
&mut self,
|
||||
psk: Option<SymKey>,
|
||||
pk: SPk,
|
||||
outfile: Option<PathBuf>,
|
||||
broker_peer: Option<BrokerPeer>,
|
||||
outwg: Option<WireguardOut>,
|
||||
hostname: Option<String>,
|
||||
) -> anyhow::Result<AppPeerPtr> {
|
||||
let PeerPtr(pn) = self.crypt.add_peer(psk, pk)?;
|
||||
@@ -677,7 +484,7 @@ impl AppServer {
|
||||
let current_endpoint = None;
|
||||
self.peers.push(AppPeer {
|
||||
outfile,
|
||||
broker_peer,
|
||||
outwg,
|
||||
initial_endpoint,
|
||||
current_endpoint,
|
||||
});
|
||||
@@ -744,17 +551,6 @@ impl AppServer {
|
||||
use crate::protocol::HandleMsgResult;
|
||||
use AppPollResult::*;
|
||||
use KeyOutputReason::*;
|
||||
|
||||
if let Some(AppServerTest {
|
||||
termination_handler: Some(terminate),
|
||||
..
|
||||
}) = &self.test_helpers
|
||||
{
|
||||
if terminate.try_recv().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
match self.poll(&mut *rx)? {
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
SendInitiation(peer) => tx_maybe_with!(peer, || self
|
||||
@@ -779,17 +575,11 @@ impl AppServer {
|
||||
}
|
||||
|
||||
ReceivedMessage(len, endpoint) => {
|
||||
let msg_result = match self.under_load {
|
||||
DoSOperation::UnderLoad => {
|
||||
self.handle_msg_under_load(&endpoint, &rx[..len], &mut *tx)
|
||||
}
|
||||
DoSOperation::Normal => self.crypt.handle_msg(&rx[..len], &mut *tx),
|
||||
};
|
||||
match msg_result {
|
||||
match self.crypt.handle_msg(&rx[..len], &mut *tx) {
|
||||
Err(ref e) => {
|
||||
self.verbose().then(|| {
|
||||
info!(
|
||||
"error processing incoming message from {}: {:?} {}",
|
||||
"error processing incoming message from {:?}: {:?} {}",
|
||||
endpoint,
|
||||
e,
|
||||
e.backtrace()
|
||||
@@ -820,40 +610,23 @@ impl AppServer {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_msg_under_load(
|
||||
&mut self,
|
||||
endpoint: &Endpoint,
|
||||
rx: &[u8],
|
||||
tx: &mut [u8],
|
||||
) -> Result<crate::protocol::HandleMsgResult> {
|
||||
match endpoint {
|
||||
Endpoint::SocketBoundAddress(socket) => {
|
||||
self.crypt.handle_msg_under_load(rx, &mut *tx, socket)
|
||||
}
|
||||
Endpoint::Discovery(_) => {
|
||||
anyhow::bail!("Host-path discovery is not supported under load")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_key(
|
||||
&mut self,
|
||||
&self,
|
||||
peer: AppPeerPtr,
|
||||
why: KeyOutputReason,
|
||||
key: &SymKey,
|
||||
) -> anyhow::Result<()> {
|
||||
let peerid = peer.lower().get(&self.crypt).pidt()?;
|
||||
let ap = peer.get_app(self);
|
||||
|
||||
if self.verbose() {
|
||||
let msg = match why {
|
||||
KeyOutputReason::Exchanged => "Exchanged key with peer",
|
||||
KeyOutputReason::Stale => "Erasing outdated key from peer",
|
||||
};
|
||||
info!("{} {}", msg, peerid.fmt_b64::<MAX_B64_PEER_ID_SIZE>());
|
||||
info!("{} {}", msg, fmt_b64(&*peerid));
|
||||
}
|
||||
|
||||
let ap = peer.get_app(self);
|
||||
|
||||
if let Some(of) = ap.outfile.as_ref() {
|
||||
// This might leave some fragments of the secret on the stack;
|
||||
// in practice this is likely not a problem because the stack likely
|
||||
@@ -862,7 +635,7 @@ impl AppServer {
|
||||
// data will linger in the linux page cache anyways with the current
|
||||
// implementation, going to great length to erase the secret here is
|
||||
// not worth it right now.
|
||||
key.store_b64::<MAX_B64_KEY_SIZE, _>(of)?;
|
||||
b64_writer(fopen_w(of)?).write_all(key.secret())?;
|
||||
let why = match why {
|
||||
KeyOutputReason::Exchanged => "exchanged",
|
||||
KeyOutputReason::Stale => "stale",
|
||||
@@ -872,11 +645,15 @@ impl AppServer {
|
||||
// it is meant to allow external detection of a successful key-exchange
|
||||
println!(
|
||||
"output-key peer {} key-file {of:?} {why}",
|
||||
peerid.fmt_b64::<MAX_B64_PEER_ID_SIZE>()
|
||||
fmt_b64(&*peerid)
|
||||
);
|
||||
}
|
||||
|
||||
peer.set_psk(self, key)?;
|
||||
if let Some(owg) = ap.outwg.as_ref() {
|
||||
self.psk_broker
|
||||
.borrow_mut()
|
||||
.set_psk(&owg.dev, owg.pk.value, *key.secret())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -933,56 +710,16 @@ impl AppServer {
|
||||
|
||||
// only poll if we drained all sockets before
|
||||
if self.all_sockets_drained {
|
||||
//Non blocked polling
|
||||
self.mio_poll
|
||||
.poll(&mut self.events, Some(Duration::from_secs(0)))?;
|
||||
|
||||
if self.events.iter().peekable().peek().is_none() {
|
||||
// if there are no events, then add to blocking poll count
|
||||
self.blocking_polls_count += 1;
|
||||
//Execute blocking poll
|
||||
self.mio_poll.poll(&mut self.events, Some(timeout))?;
|
||||
} else {
|
||||
self.non_blocking_polls_count += 1;
|
||||
}
|
||||
} else {
|
||||
self.unpolled_count += 1;
|
||||
.poll(&mut self.events, Some(timeout))
|
||||
.or_else(|e| match e.kind() {
|
||||
ErrorKind::Interrupted | ErrorKind::WouldBlock => Ok(()),
|
||||
_ => Err(e),
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(AppServerTest {
|
||||
enable_dos_permanently: true,
|
||||
..
|
||||
}) = self.test_helpers
|
||||
{
|
||||
self.under_load = DoSOperation::UnderLoad;
|
||||
} else {
|
||||
//Reset blocking poll count if waiting for more than BLOCKING_POLL_COUNT_DURATION
|
||||
if self.last_update_time.elapsed() > DURATION_UPDATE_UNDER_LOAD_STATUS {
|
||||
self.last_update_time = Instant::now();
|
||||
let total_polls = self.blocking_polls_count + self.non_blocking_polls_count;
|
||||
self.psk_broker.get_mut().poll()?;
|
||||
|
||||
let load_ratio = if total_polls > 0 {
|
||||
self.non_blocking_polls_count as f64 / total_polls as f64
|
||||
} else if self.unpolled_count > 0 {
|
||||
//There are no polls, so we are under load
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if load_ratio > UNDER_LOAD_RATIO {
|
||||
self.under_load = DoSOperation::UnderLoad;
|
||||
} else {
|
||||
self.under_load = DoSOperation::Normal;
|
||||
}
|
||||
|
||||
self.blocking_polls_count = 0;
|
||||
self.non_blocking_polls_count = 0;
|
||||
self.unpolled_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// drain all sockets
|
||||
let mut would_block_count = 0;
|
||||
for (sock_no, socket) in self.sockets.iter_mut().enumerate() {
|
||||
match socket.recv_from(buf) {
|
||||
@@ -991,10 +728,10 @@ impl AppServer {
|
||||
self.all_sockets_drained = false;
|
||||
return Ok(Some((
|
||||
n,
|
||||
Endpoint::SocketBoundAddress(SocketBoundEndpoint::new(
|
||||
SocketPtr(sock_no),
|
||||
Endpoint::SocketBoundAddress {
|
||||
socket: SocketPtr(sock_no),
|
||||
addr,
|
||||
)),
|
||||
},
|
||||
)));
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
@@ -1008,11 +745,6 @@ impl AppServer {
|
||||
// if each socket returned WouldBlock, then we drained them all at least once indeed
|
||||
self.all_sockets_drained = would_block_count == self.sockets.len();
|
||||
|
||||
// Process brokers poll
|
||||
for (_, broker) in self.brokers.store.iter_mut() {
|
||||
broker.process_poll()?;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,53 @@
|
||||
use anyhow::{bail, ensure};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::io::{BufReader, Read};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
|
||||
use anyhow::{bail, ensure, Context};
|
||||
use clap::{ArgGroup, Parser, Subcommand};
|
||||
use command_fds::{CommandFdExt, FdMapping};
|
||||
use log::{error, info};
|
||||
use rustix::fd::AsRawFd;
|
||||
use rustix::net::{socketpair, AddressFamily, SocketFlags, SocketType};
|
||||
|
||||
use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::StaticKem;
|
||||
use rosenpass_secret_memory::file::StoreSecret;
|
||||
use rosenpass_secret_memory::{
|
||||
secret_policy_try_use_memfd_secrets, secret_policy_use_only_malloc_secrets,
|
||||
};
|
||||
use rosenpass_secret_memory::Public;
|
||||
use rosenpass_util::b64::b64_reader;
|
||||
use rosenpass_util::fd::claim_fd;
|
||||
use rosenpass_util::file::{LoadValue, LoadValueB64};
|
||||
use rosenpass_wireguard_broker::brokers::native_unix::{
|
||||
NativeUnixBroker, NativeUnixBrokerConfigBaseBuilder, NativeUnixBrokerConfigBaseBuilderError,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::app_server::AppServerTest;
|
||||
use crate::app_server::{AppServer, BrokerPeer};
|
||||
use crate::app_server;
|
||||
use crate::app_server::AppServer;
|
||||
use crate::protocol::{SPk, SSk, SymKey};
|
||||
|
||||
use super::config;
|
||||
|
||||
/// struct holding all CLI arguments for `clap` crate to parse
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about)]
|
||||
pub struct CliArgs {
|
||||
/// lowest log level to show – log messages at higher levels will be omitted
|
||||
#[arg(long = "log-level", value_name = "LOG_LEVEL", group = "log-level")]
|
||||
log_level: Option<log::LevelFilter>,
|
||||
#[command(author, version, about)]
|
||||
#[clap(group(
|
||||
ArgGroup::new("psk_broker_specs")
|
||||
.args(&["psk_broker", "psk_broker_fd"]),
|
||||
))]
|
||||
pub struct Cli {
|
||||
// Path of the wireguard_psk broker socket to connect to
|
||||
#[arg(long)]
|
||||
psk_broker: Option<PathBuf>,
|
||||
|
||||
/// show verbose log output – sets log level to "debug"
|
||||
#[arg(short, long, group = "log-level")]
|
||||
verbose: bool,
|
||||
|
||||
/// show no log output – sets log level to "error"
|
||||
#[arg(short, long, group = "log-level")]
|
||||
quiet: bool,
|
||||
/// When this command is called from another process, the other process can open and bind the
|
||||
/// unix socket for the psk broker connectionto use themselves, passing it to this process. In Rust this can be achieved
|
||||
/// using the [command-fds](https://docs.rs/command-fds/latest/command_fds/) crate.
|
||||
#[arg(long)]
|
||||
psk_broker_fd: Option<i32>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: CliCommand,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
/// returns the log level filter set by CLI args
|
||||
/// returns `None` if the user did not specify any log level filter via CLI
|
||||
///
|
||||
/// NOTE: the clap feature of ["argument groups"](https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_3/index.html#argument-relations)
|
||||
/// ensures that the user can not specify more than one of the possible log level arguments.
|
||||
/// Note the `#[arg("group")]` in the [`CliArgs`] struct.
|
||||
pub fn get_log_level(&self) -> Option<log::LevelFilter> {
|
||||
if self.verbose {
|
||||
return Some(log::LevelFilter::Info);
|
||||
}
|
||||
if self.quiet {
|
||||
return Some(log::LevelFilter::Error);
|
||||
}
|
||||
if let Some(level_filter) = self.log_level {
|
||||
return Some(level_filter);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// represents a command specified via CLI
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(author, version, about, long_about)]
|
||||
pub enum CliCommand {
|
||||
/// Start Rosenpass in server mode and carry on with the key exchange
|
||||
///
|
||||
@@ -109,6 +96,7 @@ pub enum CliCommand {
|
||||
config_file: PathBuf,
|
||||
|
||||
/// Forcefully overwrite existing config file
|
||||
/// - [ ] Janepie
|
||||
#[clap(short, long)]
|
||||
force: bool,
|
||||
},
|
||||
@@ -151,22 +139,13 @@ pub enum CliCommand {
|
||||
Man,
|
||||
}
|
||||
|
||||
impl CliCommand {
|
||||
/// runs the command specified via CLI
|
||||
///
|
||||
/// ## TODO
|
||||
/// - This method consumes the [`CliCommand`] value. It might be wise to use a reference...
|
||||
pub fn run(self, test_helpers: Option<AppServerTest>) -> anyhow::Result<()> {
|
||||
//Specify secret policy
|
||||
|
||||
#[cfg(feature = "enable_memfd_alloc")]
|
||||
secret_policy_try_use_memfd_secrets();
|
||||
#[cfg(not(feature = "enable_memfd_alloc"))]
|
||||
secret_policy_use_only_malloc_secrets();
|
||||
impl Cli {
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
let cli = Self::parse();
|
||||
|
||||
use CliCommand::*;
|
||||
match self {
|
||||
Man => {
|
||||
match cli {
|
||||
Cli { command: Man, .. } => {
|
||||
let man_cmd = std::process::Command::new("man")
|
||||
.args(["1", "rosenpass"])
|
||||
.status();
|
||||
@@ -175,7 +154,10 @@ impl CliCommand {
|
||||
println!(include_str!(env!("ROSENPASS_MAN")));
|
||||
}
|
||||
}
|
||||
GenConfig { config_file, force } => {
|
||||
Cli {
|
||||
command: GenConfig { config_file, force },
|
||||
..
|
||||
} => {
|
||||
ensure!(
|
||||
force || !config_file.exists(),
|
||||
"config file {config_file:?} already exists"
|
||||
@@ -185,7 +167,10 @@ impl CliCommand {
|
||||
}
|
||||
|
||||
// Deprecated - use gen-keys instead
|
||||
Keygen { args } => {
|
||||
Cli {
|
||||
command: Keygen { args },
|
||||
..
|
||||
} => {
|
||||
log::warn!("The 'keygen' command is deprecated. Please use the 'gen-keys' command instead.");
|
||||
|
||||
let mut public_key: Option<PathBuf> = None;
|
||||
@@ -218,11 +203,15 @@ impl CliCommand {
|
||||
generate_and_save_keypair(secret_key.unwrap(), public_key.unwrap())?;
|
||||
}
|
||||
|
||||
GenKeys {
|
||||
config_file,
|
||||
public_key,
|
||||
secret_key,
|
||||
force,
|
||||
Cli {
|
||||
command:
|
||||
GenKeys {
|
||||
config_file,
|
||||
public_key,
|
||||
secret_key,
|
||||
force,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
// figure out where the key file is specified, in the config file or directly as flag?
|
||||
let (pkf, skf) = match (config_file, public_key, secret_key) {
|
||||
@@ -262,7 +251,10 @@ impl CliCommand {
|
||||
generate_and_save_keypair(skf, pkf)?;
|
||||
}
|
||||
|
||||
ExchangeConfig { config_file } => {
|
||||
ref cli @ Cli {
|
||||
command: ExchangeConfig { ref config_file },
|
||||
..
|
||||
} => {
|
||||
ensure!(
|
||||
config_file.exists(),
|
||||
"config file '{config_file:?}' does not exist"
|
||||
@@ -270,27 +262,35 @@ impl CliCommand {
|
||||
|
||||
let config = config::Rosenpass::load(config_file)?;
|
||||
config.validate()?;
|
||||
Self::event_loop(config, test_helpers)?;
|
||||
Self::event_loop(&cli, &config)?;
|
||||
}
|
||||
|
||||
Exchange {
|
||||
first_arg,
|
||||
mut rest_of_args,
|
||||
config_file,
|
||||
ref cli @ Cli {
|
||||
command:
|
||||
Exchange {
|
||||
ref first_arg,
|
||||
ref rest_of_args,
|
||||
ref config_file,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
rest_of_args.insert(0, first_arg);
|
||||
let args = rest_of_args;
|
||||
let mut args = Vec::new();
|
||||
args.push(first_arg.clone());
|
||||
args.extend_from_slice(&rest_of_args[..]);
|
||||
let mut config = config::Rosenpass::parse_args(args)?;
|
||||
|
||||
if let Some(p) = config_file {
|
||||
if let Some(p) = &config_file {
|
||||
config.store(&p)?;
|
||||
config.config_file_path = p;
|
||||
config.config_file_path = p.clone();
|
||||
}
|
||||
config.validate()?;
|
||||
Self::event_loop(config, test_helpers)?;
|
||||
Self::event_loop(&cli, &config)?;
|
||||
}
|
||||
|
||||
Validate { config_files } => {
|
||||
Cli {
|
||||
command: Validate { config_files },
|
||||
..
|
||||
} => {
|
||||
for file in config_files {
|
||||
match config::Rosenpass::load(&file) {
|
||||
Ok(config) => {
|
||||
@@ -309,55 +309,99 @@ impl CliCommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn event_loop(
|
||||
config: config::Rosenpass,
|
||||
test_helpers: Option<AppServerTest>,
|
||||
) -> anyhow::Result<()> {
|
||||
const MAX_PSK_SIZE: usize = 1000;
|
||||
fn event_loop(cli: &Cli, config: &config::Rosenpass) -> anyhow::Result<()> {
|
||||
// load own keys
|
||||
let sk = SSk::load(&config.secret_key)?;
|
||||
let pk = SPk::load(&config.public_key)?;
|
||||
|
||||
// Connect to the psk broker unix socket if one was specified
|
||||
// OR OTHERWISE pawn the psk broker and use socketpair(2) to connect with them
|
||||
let psk_broker_socket = if let Some(ref broker_path) = cli.psk_broker {
|
||||
let sock = UnixStream::connect(broker_path)?;
|
||||
sock.set_nonblocking(true)?;
|
||||
sock
|
||||
} else if let Some(broker_fd) = cli.psk_broker_fd {
|
||||
let sock = UnixStream::from(claim_fd(broker_fd)?);
|
||||
sock.set_nonblocking(true)?;
|
||||
sock
|
||||
} else {
|
||||
let (ours, theirs) = socketpair(
|
||||
AddressFamily::UNIX,
|
||||
SocketType::STREAM,
|
||||
SocketFlags::empty(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
// Setup our end of the socketpair
|
||||
let ours = UnixStream::from(ours);
|
||||
ours.set_nonblocking(true)?;
|
||||
|
||||
// Start the PSK broker
|
||||
let mut child = Command::new("rosenpass-wireguard-broker-socket-handler")
|
||||
.args(&["--stream-fd", "3"])
|
||||
.fd_mappings(vec![FdMapping {
|
||||
parent_fd: theirs.as_raw_fd(),
|
||||
child_fd: 3,
|
||||
}])?
|
||||
.spawn()?;
|
||||
|
||||
// Handle the PSK broker crashing
|
||||
thread::spawn(move || {
|
||||
let status = child.wait();
|
||||
|
||||
if let Ok(status) = status {
|
||||
if status.success() {
|
||||
// Maybe they are doing double forking?
|
||||
info!("PSK broker exited.");
|
||||
} else {
|
||||
error!("PSK broker exited with an error ({status:?})");
|
||||
}
|
||||
} else {
|
||||
error!("Wait on PSK broker process failed ({status:?})");
|
||||
}
|
||||
});
|
||||
|
||||
ours
|
||||
};
|
||||
|
||||
// start an application server
|
||||
let mut srv = std::boxed::Box::<AppServer>::new(AppServer::new(
|
||||
sk,
|
||||
pk,
|
||||
config.listen,
|
||||
config.verbosity,
|
||||
test_helpers,
|
||||
config.listen.clone(),
|
||||
psk_broker_socket,
|
||||
config.verbosity.clone(),
|
||||
)?);
|
||||
|
||||
let broker_store_ptr = srv.register_broker(Box::new(NativeUnixBroker::new()))?;
|
||||
|
||||
fn cfg_err_map(e: NativeUnixBrokerConfigBaseBuilderError) -> anyhow::Error {
|
||||
anyhow::Error::msg(format!("NativeUnixBrokerConfigBaseBuilderError: {:?}", e))
|
||||
}
|
||||
|
||||
for cfg_peer in config.peers {
|
||||
let broker_peer = if let Some(wg) = &cfg_peer.wg {
|
||||
let peer_cfg = NativeUnixBrokerConfigBaseBuilder::default()
|
||||
.peer_id_b64(&wg.peer)?
|
||||
.interface(wg.device.clone())
|
||||
.extra_params_ser(&wg.extra_params)?
|
||||
.build()
|
||||
.map_err(cfg_err_map)?;
|
||||
|
||||
let broker_peer = BrokerPeer::new(broker_store_ptr.clone(), Box::new(peer_cfg));
|
||||
|
||||
Some(broker_peer)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for cfg_peer in config.peers.iter().by_ref() {
|
||||
srv.add_peer(
|
||||
// psk, pk, outfile, outwg, tx_addr
|
||||
cfg_peer
|
||||
.pre_shared_key
|
||||
.map(SymKey::load_b64::<MAX_PSK_SIZE, _>)
|
||||
.as_ref()
|
||||
.map(SymKey::load_b64)
|
||||
.transpose()?,
|
||||
SPk::load(&cfg_peer.public_key)?,
|
||||
cfg_peer.key_out,
|
||||
broker_peer,
|
||||
cfg_peer.key_out.clone(),
|
||||
cfg_peer
|
||||
.wg
|
||||
.as_ref()
|
||||
.map(|cfg| -> anyhow::Result<_> {
|
||||
let b64pk = &cfg.peer;
|
||||
let mut pk = Public::zero();
|
||||
b64_reader(BufReader::new(b64pk.as_bytes()))
|
||||
.read_exact(&mut pk.value)
|
||||
.with_context(|| {
|
||||
format!("Could not decode base64 public key: '{b64pk}'")
|
||||
})?;
|
||||
|
||||
Ok(app_server::WireguardOut {
|
||||
pk,
|
||||
dev: cfg.device.clone(),
|
||||
extra_params: cfg.extra_params.clone(),
|
||||
})
|
||||
})
|
||||
.transpose()?,
|
||||
cfg_peer.endpoint.clone(),
|
||||
)?;
|
||||
}
|
||||
@@ -372,5 +416,5 @@ fn generate_and_save_keypair(secret_key: PathBuf, public_key: PathBuf) -> anyhow
|
||||
let mut spk = crate::protocol::SPk::random();
|
||||
StaticKem::keygen(ssk.secret_mut(), spk.secret_mut())?;
|
||||
ssk.store_secret(secret_key)?;
|
||||
spk.store(public_key)
|
||||
spk.store_secret(public_key)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
//! Configuration readable from a config file.
|
||||
//!
|
||||
//! Rosenpass supports reading its configuration from a TOML file. This module contains a struct
|
||||
//! [`Rosenpass`] which holds such a configuration.
|
||||
//!
|
||||
//! ## TODO
|
||||
//! - support `~` in <https://github.com/rosenpass/rosenpass/issues/237>
|
||||
//! - provide tooling to create config file from shell <https://github.com/rosenpass/rosenpass/issues/247>
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs,
|
||||
@@ -16,128 +7,62 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use rosenpass_util::file::{fopen_w, Visibility};
|
||||
use rosenpass_util::file::fopen_w;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Rosenpass {
|
||||
/// path to the public key file
|
||||
pub public_key: PathBuf,
|
||||
|
||||
/// path to the secret key file
|
||||
pub secret_key: PathBuf,
|
||||
|
||||
/// list of [`SocketAddr`] to listen on
|
||||
///
|
||||
/// Examples:
|
||||
/// - `0.0.0.0:123`
|
||||
pub listen: Vec<SocketAddr>,
|
||||
|
||||
/// log verbosity
|
||||
///
|
||||
/// This is subject to change. See [`Verbosity`] for details.
|
||||
#[serde(default)]
|
||||
pub verbosity: Verbosity,
|
||||
|
||||
/// list of peers
|
||||
///
|
||||
/// See the [`RosenpassPeer`] type for more information and examples.
|
||||
pub peers: Vec<RosenpassPeer>,
|
||||
|
||||
/// path to the file which provided this configuration
|
||||
///
|
||||
/// This item is of course not read from the TOML but is added by the algorithm that parses
|
||||
/// the config file.
|
||||
#[serde(skip)]
|
||||
pub config_file_path: PathBuf,
|
||||
}
|
||||
|
||||
/// ## TODO
|
||||
/// - replace this type with [`log::LevelFilter`], also see <https://github.com/rosenpass/rosenpass/pull/246>
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
|
||||
pub enum Verbosity {
|
||||
Quiet,
|
||||
Verbose,
|
||||
}
|
||||
|
||||
/// ## TODO
|
||||
/// - examples
|
||||
/// - documentation
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RosenpassPeer {
|
||||
/// path to the public key of the peer
|
||||
pub public_key: PathBuf,
|
||||
|
||||
/// ## TODO
|
||||
/// - documentation
|
||||
pub endpoint: Option<String>,
|
||||
|
||||
/// path to the pre-shared key with the peer
|
||||
///
|
||||
/// NOTE: this item can be skipped in the config if you do not use a pre-shared key with the peer
|
||||
pub pre_shared_key: Option<PathBuf>,
|
||||
|
||||
/// ## TODO
|
||||
/// - documentation
|
||||
#[serde(default)]
|
||||
pub key_out: Option<PathBuf>,
|
||||
|
||||
/// ## TODO
|
||||
/// - documentation
|
||||
/// - make this field only available on binary builds, not on library builds <https://github.com/rosenpass/rosenpass/issues/249>
|
||||
// TODO make this field only available on binary builds, not on library builds
|
||||
#[serde(flatten)]
|
||||
pub wg: Option<WireGuard>,
|
||||
}
|
||||
|
||||
/// ## TODO
|
||||
/// - documentation
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WireGuard {
|
||||
/// ## TODO
|
||||
/// - documentation
|
||||
pub device: String,
|
||||
|
||||
/// ## TODO
|
||||
/// - documentation
|
||||
pub peer: String,
|
||||
|
||||
/// ## TODO
|
||||
/// - documentation
|
||||
#[serde(default)]
|
||||
pub extra_params: Vec<String>,
|
||||
}
|
||||
|
||||
impl Rosenpass {
|
||||
/// load configuration from a TOML file
|
||||
/// Load a config file from a file path
|
||||
///
|
||||
/// NOTE: no validation is conducted, e.g. the paths specified in the configuration are not
|
||||
/// checked whether they even exist.
|
||||
///
|
||||
/// ## TODO
|
||||
/// - consider using a different algorithm to determine home directory – the below one may
|
||||
/// behave unexpectedly on Windows
|
||||
/// no validation is conducted
|
||||
pub fn load<P: AsRef<Path>>(p: P) -> anyhow::Result<Self> {
|
||||
// read file and deserialize
|
||||
let mut config: Self = toml::from_str(&fs::read_to_string(&p)?)?;
|
||||
|
||||
// resolve `~` (see https://github.com/rosenpass/rosenpass/issues/237)
|
||||
use util::resolve_path_with_tilde;
|
||||
resolve_path_with_tilde(&mut config.public_key);
|
||||
resolve_path_with_tilde(&mut config.secret_key);
|
||||
for peer in config.peers.iter_mut() {
|
||||
resolve_path_with_tilde(&mut peer.public_key);
|
||||
if let Some(ref mut psk) = &mut peer.pre_shared_key {
|
||||
resolve_path_with_tilde(psk);
|
||||
}
|
||||
if let Some(ref mut ko) = &mut peer.key_out {
|
||||
resolve_path_with_tilde(ko);
|
||||
}
|
||||
}
|
||||
|
||||
// add path to "self"
|
||||
p.as_ref().clone_into(&mut config.config_file_path);
|
||||
|
||||
// return
|
||||
config.config_file_path = p.as_ref().to_owned();
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -151,29 +76,25 @@ impl Rosenpass {
|
||||
|
||||
/// Commit the configuration to where it came from, overwriting the original file
|
||||
pub fn commit(&self) -> anyhow::Result<()> {
|
||||
let mut f = fopen_w(&self.config_file_path, Visibility::Public)?;
|
||||
let mut f = fopen_w(&self.config_file_path)?;
|
||||
f.write_all(toml::to_string_pretty(&self)?.as_bytes())?;
|
||||
|
||||
self.store(&self.config_file_path)
|
||||
}
|
||||
|
||||
/// Validate a configuration
|
||||
///
|
||||
/// ## TODO
|
||||
/// - check that files do not just exist but are also readable
|
||||
/// - warn if neither out_key nor exchange_command of a peer is defined (v.i.)
|
||||
pub fn validate(&self) -> anyhow::Result<()> {
|
||||
// check the public key file exists
|
||||
// check the public-key file exists
|
||||
ensure!(
|
||||
self.public_key.is_file(),
|
||||
"could not find public-key file {:?}: no such file",
|
||||
"public-key file {:?} does not exist",
|
||||
self.public_key
|
||||
);
|
||||
|
||||
// check the secret-key file exists
|
||||
ensure!(
|
||||
self.secret_key.is_file(),
|
||||
"could not find secret-key file {:?}: no such file",
|
||||
"secret-key file {:?} does not exist",
|
||||
self.secret_key
|
||||
);
|
||||
|
||||
@@ -448,9 +369,10 @@ impl Default for Verbosity {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn split_str(s: &str) -> Vec<String> {
|
||||
s.split(' ').map(|s| s.to_string()).collect()
|
||||
}
|
||||
@@ -520,67 +442,3 @@ mod test {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod util {
|
||||
use std::path::PathBuf;
|
||||
/// takes a path that can potentially start with a `~` and resolves that `~` to the user's home directory
|
||||
///
|
||||
/// ## Example
|
||||
/// ```
|
||||
/// use rosenpass::config::util::resolve_path_with_tilde;
|
||||
/// std::env::set_var("HOME","/home/dummy");
|
||||
/// let mut path = std::path::PathBuf::from("~/foo.toml");
|
||||
/// resolve_path_with_tilde(&mut path);
|
||||
/// assert!(path == std::path::PathBuf::from("/home/dummy/foo.toml"));
|
||||
/// ```
|
||||
pub fn resolve_path_with_tilde(path: &mut PathBuf) {
|
||||
if let Some(first_segment) = path.iter().next() {
|
||||
if !path.has_root() && first_segment == "~" {
|
||||
let home_dir = home::home_dir().unwrap_or_else(|| {
|
||||
log::error!("config file contains \"~\" but can not determine home diretory");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let orig_path = path.clone();
|
||||
path.clear();
|
||||
path.push(home_dir);
|
||||
for segment in orig_path.iter().skip(1) {
|
||||
path.push(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_resolve_path_with_tilde() {
|
||||
let test = |path_str: &str, resolved: &str| {
|
||||
let mut path = PathBuf::from(path_str);
|
||||
resolve_path_with_tilde(&mut path);
|
||||
assert!(
|
||||
path == PathBuf::from(resolved),
|
||||
"Path {:?} has been resolved to {:?} but should have been resolved to {:?}.",
|
||||
path_str,
|
||||
path,
|
||||
resolved
|
||||
);
|
||||
};
|
||||
// set environment because otherwise the test result would depend on the system running this
|
||||
std::env::set_var("USER", "dummy");
|
||||
std::env::set_var("HOME", "/home/dummy");
|
||||
|
||||
// should resolve
|
||||
test("~/foo.toml", "/home/dummy/foo.toml");
|
||||
test("~//foo", "/home/dummy/foo");
|
||||
test("~/../other_user/foo", "/home/dummy/../other_user/foo");
|
||||
|
||||
// should _not_ resolve
|
||||
test("~foo/bar", "~foo/bar");
|
||||
test(".~/foo", ".~/foo");
|
||||
test("/~/foo.toml", "/~/foo.toml");
|
||||
test(r"~\foo", r"~\foo");
|
||||
test(r"C:\~\foo.toml", r"C:\~\foo.toml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ pub fn protocol() -> Result<HashDomain> {
|
||||
|
||||
hash_domain_ns!(protocol, mac, "mac");
|
||||
hash_domain_ns!(protocol, cookie, "cookie");
|
||||
hash_domain_ns!(protocol, cookie_value, "cookie-value");
|
||||
hash_domain_ns!(protocol, cookie_key, "cookie-key");
|
||||
hash_domain_ns!(protocol, peerid, "peer id");
|
||||
hash_domain_ns!(protocol, biscuit_ad, "biscuit additional data");
|
||||
hash_domain_ns!(protocol, ckinit, "chaining key init");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use rosenpass_lenses::LenseError;
|
||||
|
||||
pub mod app_server;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
@@ -12,3 +14,11 @@ pub enum RosenpassError {
|
||||
#[error("invalid message type")]
|
||||
InvalidMessageType(u8),
|
||||
}
|
||||
|
||||
impl From<LenseError> for RosenpassError {
|
||||
fn from(value: LenseError) -> Self {
|
||||
match value {
|
||||
LenseError::BufferSizeMismatch => RosenpassError::BufferSizeMismatch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
use clap::Parser;
|
||||
use log::error;
|
||||
use rosenpass::cli::CliArgs;
|
||||
use rosenpass::cli::Cli;
|
||||
use std::process::exit;
|
||||
|
||||
/// Catches errors, prints them through the logger, then exits
|
||||
pub fn main() {
|
||||
// parse CLI arguments
|
||||
let args = CliArgs::parse();
|
||||
// default to displaying warning and error log messages only
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
|
||||
// init logging
|
||||
{
|
||||
let mut log_builder = env_logger::Builder::from_default_env(); // sets log level filter from environment (or defaults)
|
||||
if let Some(level) = args.get_log_level() {
|
||||
log::debug!("setting log level to {:?} (set via CLI parameter)", level);
|
||||
log_builder.filter_level(level); // set log level filter from CLI args if available
|
||||
}
|
||||
log_builder.init();
|
||||
|
||||
// // check the effectiveness of the log level filter with the following lines:
|
||||
// use log::{debug, error, info, trace, warn};
|
||||
// trace!("trace dummy");
|
||||
// debug!("debug dummy");
|
||||
// info!("info dummy");
|
||||
// warn!("warn dummy");
|
||||
// error!("error dummy");
|
||||
}
|
||||
|
||||
match args.command.run(None) {
|
||||
match Cli::run() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
error!("{e:?}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,128 +6,129 @@
|
||||
//! always serialized instance of the data in question. This is closely related
|
||||
//! to the concept of lenses in function programming; more on that here:
|
||||
//! [https://sinusoid.es/misc/lager/lenses.pdf](https://sinusoid.es/misc/lager/lenses.pdf)
|
||||
//! To achieve this we utilize the zerocopy library.
|
||||
//!
|
||||
use std::mem::size_of;
|
||||
use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
//! # Example
|
||||
//!
|
||||
//! The following example uses the [`lense` macro](rosenpass_lenses::lense) to create a lense that
|
||||
//! might be useful when dealing with UDP headers.
|
||||
//!
|
||||
//! ```
|
||||
//! use rosenpass_lenses::{lense, LenseView};
|
||||
//! use rosenpass::RosenpassError;
|
||||
//! # fn main() -> Result<(), RosenpassError> {
|
||||
//!
|
||||
//! lense! {UdpDatagramHeader :=
|
||||
//! source_port: 2,
|
||||
//! dest_port: 2,
|
||||
//! length: 2,
|
||||
//! checksum: 2
|
||||
//! }
|
||||
//!
|
||||
//! let mut buf = [0u8; 8];
|
||||
//!
|
||||
//! // read-only lense, no check of size:
|
||||
//! let lense = UdpDatagramHeader(&buf);
|
||||
//! assert_eq!(lense.checksum(), &[0, 0]);
|
||||
//!
|
||||
//! // mutable lense, runtime check of size
|
||||
//! let mut lense = buf.as_mut().udp_datagram_header()?;
|
||||
//! lense.source_port_mut().copy_from_slice(&53u16.to_be_bytes()); // some DNS, anyone?
|
||||
//!
|
||||
//! // the original buffer is still available
|
||||
//! assert_eq!(buf, [0, 53, 0, 0, 0, 0, 0, 0]);
|
||||
//!
|
||||
//! // read-only lense, runtime check of size
|
||||
//! let lense = buf.as_ref().udp_datagram_header()?;
|
||||
//! assert_eq!(lense.source_port(), &[0, 53]);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use super::RosenpassError;
|
||||
use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::{EphemeralKem, StaticKem};
|
||||
use rosenpass_ciphers::{aead, xaead, KEY_LEN};
|
||||
pub const MSG_SIZE_LEN: usize = 1;
|
||||
pub const RESERVED_LEN: usize = 3;
|
||||
pub const MAC_SIZE: usize = 16;
|
||||
pub const COOKIE_SIZE: usize = 16;
|
||||
pub const SID_LEN: usize = 4;
|
||||
use rosenpass_lenses::{lense, LenseView};
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct Envelope<M: AsBytes + FromBytes> {
|
||||
// Macro magic ////////////////////////////////////////////////////////////////
|
||||
|
||||
lense! { Envelope<M> :=
|
||||
/// [MsgType] of this message
|
||||
pub msg_type: u8,
|
||||
msg_type: 1,
|
||||
/// Reserved for future use
|
||||
pub reserved: [u8; 3],
|
||||
reserved: 3,
|
||||
/// The actual Paylod
|
||||
pub payload: M,
|
||||
payload: M::LEN,
|
||||
/// Message Authentication Code (mac) over all bytes until (exclusive)
|
||||
/// `mac` itself
|
||||
pub mac: [u8; 16],
|
||||
mac: 16,
|
||||
/// Currently unused, TODO: do something with this
|
||||
pub cookie: [u8; 16],
|
||||
cookie: 16
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct InitHello {
|
||||
lense! { InitHello :=
|
||||
/// Randomly generated connection id
|
||||
pub sidi: [u8; 4],
|
||||
sidi: 4,
|
||||
/// Kyber 512 Ephemeral Public Key
|
||||
pub epki: [u8; EphemeralKem::PK_LEN],
|
||||
epki: EphemeralKem::PK_LEN,
|
||||
/// Classic McEliece Ciphertext
|
||||
pub sctr: [u8; StaticKem::CT_LEN],
|
||||
sctr: StaticKem::CT_LEN,
|
||||
/// Encryped: 16 byte hash of McEliece initiator static key
|
||||
pub pidic: [u8; aead::TAG_LEN + 32],
|
||||
pidic: aead::TAG_LEN + 32,
|
||||
/// Encrypted TAI64N Time Stamp (against replay attacks)
|
||||
pub auth: [u8; aead::TAG_LEN],
|
||||
auth: aead::TAG_LEN
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct RespHello {
|
||||
lense! { RespHello :=
|
||||
/// Randomly generated connection id
|
||||
pub sidr: [u8; 4],
|
||||
sidr: 4,
|
||||
/// Copied from InitHello
|
||||
pub sidi: [u8; 4],
|
||||
sidi: 4,
|
||||
/// Kyber 512 Ephemeral Ciphertext
|
||||
pub ecti: [u8; EphemeralKem::CT_LEN],
|
||||
ecti: EphemeralKem::CT_LEN,
|
||||
/// Classic McEliece Ciphertext
|
||||
pub scti: [u8; StaticKem::CT_LEN],
|
||||
scti: StaticKem::CT_LEN,
|
||||
/// Empty encrypted message (just an auth tag)
|
||||
pub auth: [u8; aead::TAG_LEN],
|
||||
auth: aead::TAG_LEN,
|
||||
/// Responders handshake state in encrypted form
|
||||
pub biscuit: [u8; BISCUIT_CT_LEN],
|
||||
biscuit: BISCUIT_CT_LEN
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct InitConf {
|
||||
lense! { InitConf :=
|
||||
/// Copied from InitHello
|
||||
pub sidi: [u8; 4],
|
||||
sidi: 4,
|
||||
/// Copied from RespHello
|
||||
pub sidr: [u8; 4],
|
||||
sidr: 4,
|
||||
/// Responders handshake state in encrypted form
|
||||
pub biscuit: [u8; BISCUIT_CT_LEN],
|
||||
biscuit: BISCUIT_CT_LEN,
|
||||
/// Empty encrypted message (just an auth tag)
|
||||
pub auth: [u8; aead::TAG_LEN],
|
||||
auth: aead::TAG_LEN
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct EmptyData {
|
||||
lense! { EmptyData :=
|
||||
/// Copied from RespHello
|
||||
pub sid: [u8; 4],
|
||||
sid: 4,
|
||||
/// Nonce
|
||||
pub ctr: [u8; 8],
|
||||
ctr: 8,
|
||||
/// Empty encrypted message (just an auth tag)
|
||||
pub auth: [u8; aead::TAG_LEN],
|
||||
auth: aead::TAG_LEN
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct Biscuit {
|
||||
lense! { Biscuit :=
|
||||
/// H(spki) – Ident ifies the initiator
|
||||
pub pidi: [u8; KEY_LEN],
|
||||
pidi: KEY_LEN,
|
||||
/// The biscuit number (replay protection)
|
||||
pub biscuit_no: [u8; 12],
|
||||
biscuit_no: 12,
|
||||
/// Chaining key
|
||||
pub ck: [u8; KEY_LEN],
|
||||
ck: KEY_LEN
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct DataMsg {
|
||||
pub dummy: [u8; 4],
|
||||
lense! { DataMsg :=
|
||||
dummy: 4
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct CookieReplyInner {
|
||||
/// [MsgType] of this message
|
||||
pub msg_type: u8,
|
||||
/// Reserved for future use
|
||||
pub reserved: [u8; 3],
|
||||
/// Session ID of the sender (initiator)
|
||||
pub sid: [u8; 4],
|
||||
/// Encrypted cookie with authenticated initiator `mac`
|
||||
pub cookie_encrypted: [u8; xaead::NONCE_LEN + COOKIE_SIZE + xaead::TAG_LEN],
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct CookieReply {
|
||||
pub inner: CookieReplyInner,
|
||||
pub padding: [u8; size_of::<Envelope<InitHello>>() - size_of::<CookieReplyInner>()],
|
||||
lense! { CookieReply :=
|
||||
dummy: 4
|
||||
}
|
||||
|
||||
// Traits /////////////////////////////////////////////////////////////////////
|
||||
@@ -176,14 +177,8 @@ impl TryFrom<u8> for MsgType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MsgType> for u8 {
|
||||
fn from(val: MsgType) -> Self {
|
||||
val as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// length in bytes of an unencrypted Biscuit (plain text)
|
||||
pub const BISCUIT_PT_LEN: usize = size_of::<Biscuit>();
|
||||
pub const BISCUIT_PT_LEN: usize = Biscuit::<()>::LEN;
|
||||
|
||||
/// Length in bytes of an encrypted Biscuit (cipher text)
|
||||
pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + xaead::NONCE_LEN + xaead::TAG_LEN;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,4 @@
|
||||
use std::{
|
||||
fs,
|
||||
net::UdpSocket,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use rosenpass::{app_server::AppServerTestBuilder, cli::CliArgs};
|
||||
use rosenpass_secret_memory::{Public, Secret};
|
||||
use rosenpass_wireguard_broker::{WireguardBrokerMio, WG_KEY_LEN, WG_PEER_LEN};
|
||||
use serial_test::serial;
|
||||
use std::io::Write;
|
||||
use std::{fs, net::UdpSocket, path::PathBuf, process::Stdio, time::Duration};
|
||||
|
||||
const BIN: &str = "rosenpass";
|
||||
|
||||
@@ -41,29 +28,26 @@ fn generate_keys() {
|
||||
fs::remove_dir_all(&tmpdir).unwrap();
|
||||
}
|
||||
|
||||
fn find_udp_socket() -> Option<u16> {
|
||||
(1025..=u16::MAX).find(|&port| UdpSocket::bind(("::1", port)).is_ok())
|
||||
fn find_udp_socket() -> u16 {
|
||||
for port in 1025..=u16::MAX {
|
||||
if UdpSocket::bind(("127.0.0.1", port)).is_ok() {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
panic!("no free UDP port found");
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut log_builder = env_logger::Builder::from_default_env(); // sets log level filter from environment (or defaults)
|
||||
log_builder.filter_level(log::LevelFilter::Debug);
|
||||
log_builder.format_timestamp_nanos();
|
||||
log_builder.format(|buf, record| {
|
||||
let ts_format = buf.timestamp_nanos().to_string();
|
||||
writeln!(
|
||||
buf,
|
||||
"\x1b[1m{:?}\x1b[0m {}: {}",
|
||||
std::thread::current().id(),
|
||||
&ts_format[14..],
|
||||
record.args()
|
||||
)
|
||||
});
|
||||
// check that we can exchange keys
|
||||
#[test]
|
||||
fn check_exchange() {
|
||||
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange");
|
||||
fs::create_dir_all(&tmpdir).unwrap();
|
||||
|
||||
let _ = log_builder.try_init();
|
||||
}
|
||||
let secret_key_paths = [tmpdir.join("secret-key-0"), tmpdir.join("secret-key-1")];
|
||||
let public_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")];
|
||||
let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")];
|
||||
|
||||
fn generate_key_pairs(secret_key_paths: &[PathBuf], public_key_paths: &[PathBuf]) {
|
||||
// generate key pairs
|
||||
for (secret_key_path, pub_key_path) in secret_key_paths.iter().zip(public_key_paths.iter()) {
|
||||
let output = test_bin::get_test_bin(BIN)
|
||||
.args(["gen-keys", "--secret-key"])
|
||||
@@ -77,115 +61,48 @@ fn generate_key_pairs(secret_key_paths: &[PathBuf], public_key_paths: &[PathBuf]
|
||||
assert!(secret_key_path.is_file());
|
||||
assert!(pub_key_path.is_file());
|
||||
}
|
||||
}
|
||||
|
||||
fn run_server_client_exchange(
|
||||
(server_cmd, server_test_builder): (&std::process::Command, AppServerTestBuilder),
|
||||
(client_cmd, client_test_builder): (&std::process::Command, AppServerTestBuilder),
|
||||
) {
|
||||
let (server_terminate, server_terminate_rx) = std::sync::mpsc::channel();
|
||||
let (client_terminate, client_terminate_rx) = std::sync::mpsc::channel();
|
||||
// start first process, the server
|
||||
let port = find_udp_socket();
|
||||
let listen_addr = format!("localhost:{port}");
|
||||
let mut server = test_bin::get_test_bin(BIN)
|
||||
.args(["exchange", "secret-key"])
|
||||
.arg(&secret_key_paths[0])
|
||||
.arg("public-key")
|
||||
.arg(&public_key_paths[0])
|
||||
.args(["listen", &listen_addr, "verbose", "peer", "public-key"])
|
||||
.arg(&public_key_paths[1])
|
||||
.arg("outfile")
|
||||
.arg(&shared_key_paths[0])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("Failed to start {BIN}");
|
||||
|
||||
let cli = CliArgs::try_parse_from(
|
||||
[server_cmd.get_program()]
|
||||
.into_iter()
|
||||
.chain(server_cmd.get_args()),
|
||||
)
|
||||
.unwrap();
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
std::thread::spawn(move || {
|
||||
cli.command
|
||||
.run(Some(
|
||||
server_test_builder
|
||||
.termination_handler(Some(server_terminate_rx))
|
||||
.build()
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
});
|
||||
// start second process, the client
|
||||
let mut client = test_bin::get_test_bin(BIN)
|
||||
.args(["exchange", "secret-key"])
|
||||
.arg(&secret_key_paths[1])
|
||||
.arg("public-key")
|
||||
.arg(&public_key_paths[1])
|
||||
.args(["verbose", "peer", "public-key"])
|
||||
.arg(&public_key_paths[0])
|
||||
.args(["endpoint", &listen_addr])
|
||||
.arg("outfile")
|
||||
.arg(&shared_key_paths[1])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("Failed to start {BIN}");
|
||||
|
||||
let cli = CliArgs::try_parse_from(
|
||||
[client_cmd.get_program()]
|
||||
.into_iter()
|
||||
.chain(client_cmd.get_args()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
cli.command
|
||||
.run(Some(
|
||||
client_test_builder
|
||||
.termination_handler(Some(client_terminate_rx))
|
||||
.build()
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// give them some time to do the key exchange under load
|
||||
std::thread::sleep(Duration::from_secs(10));
|
||||
// give them some time to do the key exchange
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
|
||||
// time's up, kill the childs
|
||||
server_terminate.send(()).unwrap();
|
||||
client_terminate.send(()).unwrap();
|
||||
}
|
||||
|
||||
// check that we can exchange keys
|
||||
#[test]
|
||||
#[serial]
|
||||
fn check_exchange_under_normal() {
|
||||
setup_logging();
|
||||
|
||||
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange");
|
||||
fs::create_dir_all(&tmpdir).unwrap();
|
||||
|
||||
let secret_key_paths = [tmpdir.join("secret-key-0"), tmpdir.join("secret-key-1")];
|
||||
let public_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")];
|
||||
let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")];
|
||||
|
||||
// generate key pairs
|
||||
generate_key_pairs(&secret_key_paths, &public_key_paths);
|
||||
|
||||
// start first process, the server
|
||||
let port = loop {
|
||||
if let Some(port) = find_udp_socket() {
|
||||
break port;
|
||||
}
|
||||
};
|
||||
|
||||
let listen_addr = format!("::1:{port}");
|
||||
let mut server_cmd = std::process::Command::new(BIN);
|
||||
|
||||
server_cmd
|
||||
.args(["exchange", "secret-key"])
|
||||
.arg(&secret_key_paths[0])
|
||||
.arg("public-key")
|
||||
.arg(&public_key_paths[0])
|
||||
.args(["listen", &listen_addr, "verbose", "peer", "public-key"])
|
||||
.arg(&public_key_paths[1])
|
||||
.arg("outfile")
|
||||
.arg(&shared_key_paths[0]);
|
||||
|
||||
let server_test_builder = AppServerTestBuilder::default();
|
||||
|
||||
let mut client_cmd = std::process::Command::new(BIN);
|
||||
client_cmd
|
||||
.args(["exchange", "secret-key"])
|
||||
.arg(&secret_key_paths[1])
|
||||
.arg("public-key")
|
||||
.arg(&public_key_paths[1])
|
||||
.args(["verbose", "peer", "public-key"])
|
||||
.arg(&public_key_paths[0])
|
||||
.args(["endpoint", &listen_addr])
|
||||
.arg("outfile")
|
||||
.arg(&shared_key_paths[1]);
|
||||
|
||||
let client_test_builder = AppServerTestBuilder::default();
|
||||
|
||||
run_server_client_exchange(
|
||||
(&server_cmd, server_test_builder),
|
||||
(&client_cmd, client_test_builder),
|
||||
);
|
||||
server.kill().unwrap();
|
||||
client.kill().unwrap();
|
||||
|
||||
// read the shared keys they created
|
||||
let shared_keys: Vec<_> = shared_key_paths
|
||||
@@ -200,132 +117,3 @@ fn check_exchange_under_normal() {
|
||||
// cleanup
|
||||
fs::remove_dir_all(&tmpdir).unwrap();
|
||||
}
|
||||
|
||||
// check that we can trigger a DoS condition and we can exchange keys under DoS
|
||||
// This test creates a responder (server) with the feature flag "integration_test_always_under_load" to always be under load condition for the test.
|
||||
#[test]
|
||||
#[serial]
|
||||
fn check_exchange_under_dos() {
|
||||
setup_logging();
|
||||
|
||||
//Generate binary with responder with feature integration_test
|
||||
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange-dos");
|
||||
fs::create_dir_all(&tmpdir).unwrap();
|
||||
|
||||
let secret_key_paths = [tmpdir.join("secret-key-0"), tmpdir.join("secret-key-1")];
|
||||
let public_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")];
|
||||
let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")];
|
||||
|
||||
// generate key pairs
|
||||
generate_key_pairs(&secret_key_paths, &public_key_paths);
|
||||
|
||||
// start first process, the server
|
||||
let port = loop {
|
||||
if let Some(port) = find_udp_socket() {
|
||||
break port;
|
||||
}
|
||||
};
|
||||
|
||||
let listen_addr = format!("::1:{port}");
|
||||
|
||||
let mut server_cmd = std::process::Command::new(BIN);
|
||||
|
||||
server_cmd
|
||||
.args(["exchange", "secret-key"])
|
||||
.arg(&secret_key_paths[0])
|
||||
.arg("public-key")
|
||||
.arg(&public_key_paths[0])
|
||||
.args(["listen", &listen_addr, "verbose", "peer", "public-key"])
|
||||
.arg(&public_key_paths[1])
|
||||
.arg("outfile")
|
||||
.arg(&shared_key_paths[0]);
|
||||
|
||||
let server_test_builder = AppServerTestBuilder::default().enable_dos_permanently(true);
|
||||
|
||||
let mut client_cmd = std::process::Command::new(BIN);
|
||||
client_cmd
|
||||
.args(["exchange", "secret-key"])
|
||||
.arg(&secret_key_paths[1])
|
||||
.arg("public-key")
|
||||
.arg(&public_key_paths[1])
|
||||
.args(["verbose", "peer", "public-key"])
|
||||
.arg(&public_key_paths[0])
|
||||
.args(["endpoint", &listen_addr])
|
||||
.arg("outfile")
|
||||
.arg(&shared_key_paths[1]);
|
||||
|
||||
let client_test_builder = AppServerTestBuilder::default();
|
||||
|
||||
run_server_client_exchange(
|
||||
(&server_cmd, server_test_builder),
|
||||
(&client_cmd, client_test_builder),
|
||||
);
|
||||
|
||||
// read the shared keys they created
|
||||
let shared_keys: Vec<_> = shared_key_paths
|
||||
.iter()
|
||||
.map(|p| fs::read_to_string(p).unwrap())
|
||||
.collect();
|
||||
|
||||
// check that they created two equal keys
|
||||
assert_eq!(shared_keys.len(), 2);
|
||||
assert_eq!(shared_keys[0], shared_keys[1]);
|
||||
|
||||
// cleanup
|
||||
fs::remove_dir_all(&tmpdir).unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Default)]
|
||||
struct MockBrokerInner {
|
||||
psk: Option<Secret<WG_KEY_LEN>>,
|
||||
peer_id: Option<Public<WG_PEER_LEN>>,
|
||||
interface: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockBroker {
|
||||
inner: Arc<Mutex<MockBrokerInner>>,
|
||||
}
|
||||
|
||||
impl WireguardBrokerMio for MockBroker {
|
||||
type MioError = anyhow::Error;
|
||||
|
||||
fn register(
|
||||
&mut self,
|
||||
_registry: &mio::Registry,
|
||||
_token: mio::Token,
|
||||
) -> Result<(), Self::MioError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_poll(&mut self) -> Result<(), Self::MioError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister(&mut self, _registry: &mio::Registry) -> Result<(), Self::MioError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl rosenpass_wireguard_broker::WireGuardBroker for MockBroker {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn set_psk(
|
||||
&mut self,
|
||||
config: rosenpass_wireguard_broker::SerializedBrokerConfig<'_>,
|
||||
) -> Result<(), Self::Error> {
|
||||
loop {
|
||||
let mut lock = self.inner.try_lock();
|
||||
|
||||
if let Ok(ref mut mutex) = lock {
|
||||
**mutex = MockBrokerInner {
|
||||
psk: Some(config.psk.clone()),
|
||||
peer_id: Some(config.peer_id.clone()),
|
||||
interface: Some(std::str::from_utf8(config.interface).unwrap().to_string()),
|
||||
};
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
391
rp
Executable file
391
rp
Executable file
@@ -0,0 +1,391 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# String formatting subsystem
|
||||
|
||||
formatting_init() {
|
||||
endl=$'\n'
|
||||
}
|
||||
|
||||
enquote() {
|
||||
while (( $# > 1 )); do
|
||||
printf "%q " "${1}"; shift
|
||||
done
|
||||
if (( $# == 1 )); then
|
||||
printf "%q" "${1}"; shift
|
||||
fi
|
||||
}
|
||||
|
||||
multiline() {
|
||||
# shellcheck disable=SC1004
|
||||
echo "${1} " | awk '
|
||||
function pm(a, b, l) {
|
||||
return length(a) > l \
|
||||
&& length(b) > l \
|
||||
&& substr(a, 1, l+1) == substr(b, 1, l+1) \
|
||||
? pm(a, b, l+1) : l;
|
||||
}
|
||||
|
||||
!started && $0 !~ /^[ \t]*$/ {
|
||||
started=1
|
||||
match($0, /^[ \t]*/)
|
||||
prefix=substr($0, 1, RLENGTH)
|
||||
}
|
||||
|
||||
started {
|
||||
print(substr($0, 1 + pm($0, prefix)));
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
dbg() {
|
||||
echo >&2 "$@"
|
||||
}
|
||||
|
||||
|
||||
detect_git_dir() {
|
||||
# https://stackoverflow.com/questions/3618078/pipe-only-stderr-through-a-filter
|
||||
(
|
||||
git -C "${scriptdir}" rev-parse --show-toplevel 3>&1 1>&2 2>&3 3>&- \
|
||||
| sed '
|
||||
/not a git repository/d;
|
||||
s/^/WARNING: /'
|
||||
) 3>&1 1>&2 2>&3 3>&-
|
||||
}
|
||||
|
||||
# Cleanup subsystem (sigterm)
|
||||
|
||||
cleanup_init() {
|
||||
cleanup_actions=()
|
||||
trap cleanup_apply exit
|
||||
}
|
||||
|
||||
cleanup_apply() {
|
||||
local f
|
||||
for f in "${cleanup_actions[@]}"; do
|
||||
eval "${f}"
|
||||
done
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
cleanup_actions+=("$(multiline "${1}")")
|
||||
}
|
||||
|
||||
# Transactional execution subsystem
|
||||
|
||||
frag_init() {
|
||||
explain=0
|
||||
frag_transaction=()
|
||||
frag "
|
||||
#! /bin/bash
|
||||
set -e"
|
||||
}
|
||||
|
||||
frag_apply() {
|
||||
local f
|
||||
for f in "${frag_transaction[@]}"; do
|
||||
if (( explain == 1 )); then
|
||||
dbg "${f}"
|
||||
fi
|
||||
eval "${f}"
|
||||
done
|
||||
}
|
||||
|
||||
frag() {
|
||||
frag_transaction+=("$(multiline "${1}")")
|
||||
}
|
||||
|
||||
frag_append() {
|
||||
local len; len="${#frag_transaction[@]}"
|
||||
frag_transaction=("${frag_transaction[@]:0:len-1}" "${frag_transaction[len-1]}${1}")
|
||||
}
|
||||
|
||||
frag_append_esc() {
|
||||
frag_append " \\${endl}${1}"
|
||||
}
|
||||
|
||||
# Usage documentation subsystem
|
||||
usage_init() {
|
||||
usagestack=("${script}")
|
||||
}
|
||||
|
||||
usage_snap() {
|
||||
echo "${#usagestack}"
|
||||
}
|
||||
|
||||
usage_restore() {
|
||||
local n; n="${1}"
|
||||
dbg REST "${1}"
|
||||
usagestack=("${usagestack[@]:0:n-2}")
|
||||
}
|
||||
|
||||
|
||||
usage() {
|
||||
dbg "Usage: ${usagestack[*]}"
|
||||
}
|
||||
|
||||
fatal() {
|
||||
dbg "FATAL: $*"
|
||||
usage
|
||||
exit 1
|
||||
}
|
||||
|
||||
genkey() {
|
||||
usagestack+=("PRIVATE_KEYS_DIR")
|
||||
local skdir
|
||||
skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"
|
||||
|
||||
while (( $# > 0 )); do
|
||||
local arg; arg="$1"; shift
|
||||
case "${arg}" in
|
||||
-h | -help | --help | help) usage; return 0 ;;
|
||||
*) fatal "Unknown option ${arg}";;
|
||||
esac
|
||||
done
|
||||
|
||||
if test -e "${skdir}"; then
|
||||
fatal "PRIVATE_KEYS_DIR \"${skdir}\" already exists"
|
||||
fi
|
||||
|
||||
frag "
|
||||
umask 077
|
||||
mkdir -p $(enquote "${skdir}")
|
||||
wg genkey > $(enquote "${skdir}"/wgsk)
|
||||
$(enquote "${binary}") gen-keys \\
|
||||
-s $(enquote "${skdir}"/pqsk) \\
|
||||
-p $(enquote "${skdir}"/pqpk)"
|
||||
}
|
||||
|
||||
pubkey() {
|
||||
usagestack+=("PRIVATE_KEYS_DIR" "PUBLIC_KEYS_DIR")
|
||||
local skdir pkdir
|
||||
skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"
|
||||
pkdir="${1%/}"; shift || fatal "Required positional argument: PUBLIC_KEYS_DIR"
|
||||
|
||||
while (( $# > 0 )); do
|
||||
local arg; arg="$1"; shift
|
||||
case "${arg}" in
|
||||
-h | -help | --help | help) usage; exit 0;;
|
||||
*) fatal "Unknown option ${arg}";;
|
||||
esac
|
||||
done
|
||||
|
||||
if test -e "${pkdir}"; then
|
||||
fatal "PUBLIC_KEYS_DIR \"${pkdir}\" already exists"
|
||||
fi
|
||||
|
||||
frag "
|
||||
mkdir -p $(enquote "${pkdir}")
|
||||
wg pubkey < $(enquote "${skdir}"/wgsk) > $(enquote "${pkdir}/wgpk")
|
||||
cp $(enquote "${skdir}"/pqpk) $(enquote "${pkdir}/pqpk")"
|
||||
}
|
||||
|
||||
exchange() {
|
||||
usagestack+=("PRIVATE_KEYS_DIR" "[dev <device>]" "[listen <ip>:<port>]" "[peer PUBLIC_KEYS_DIR [endpoint <ip>:<port>] [persistent-keepalive <interval>] [allowed-ips <ip1>/<cidr1>[,<ip2>/<cidr2>]...]]...")
|
||||
local skdir dev lport
|
||||
dev="${project_name}0"
|
||||
skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"
|
||||
|
||||
while (( $# > 0 )); do
|
||||
local arg; arg="$1"; shift
|
||||
case "${arg}" in
|
||||
dev) dev="${1}"; shift || fatal "dev option requires parameter";;
|
||||
peer) set -- "peer" "$@"; break;; # Parsed down below
|
||||
listen)
|
||||
local listen; listen="${1}";
|
||||
lip="${listen%:*}";
|
||||
lport="${listen/*:/}";
|
||||
if [[ "$lip" = "$lport" ]]; then
|
||||
lip="[::]"
|
||||
fi
|
||||
shift;;
|
||||
-h | -help | --help | help) usage; return 0;;
|
||||
*) fatal "Unknown option ${arg}";;
|
||||
esac
|
||||
done
|
||||
|
||||
if (( $# == 0 )); then
|
||||
fatal "Needs at least one peer specified"
|
||||
fi
|
||||
|
||||
# os dependent setup
|
||||
case "$OSTYPE" in
|
||||
linux-*) # could be linux-gnu or linux-musl
|
||||
frag "
|
||||
# Create the WireGuard interface
|
||||
ip link add dev $(enquote "${dev}") type wireguard || true"
|
||||
|
||||
cleanup "
|
||||
ip link del dev $(enquote "${dev}") || true"
|
||||
|
||||
frag "
|
||||
ip link set dev $(enquote "${dev}") up"
|
||||
;;
|
||||
|
||||
freebsd*)
|
||||
frag "
|
||||
# load the WireGuard kernel module
|
||||
kldload -n if_wg || fatal 'Cannot load if_wg kernel module'"
|
||||
|
||||
frag "
|
||||
# Create the WireGuard interface
|
||||
ifconfig wg create name $(enquote "${dev}") || true"
|
||||
|
||||
cleanup "
|
||||
ifconfig $(enquote "${dev}") destroy || true"
|
||||
|
||||
frag "
|
||||
ifconfig $(enquote "${dev}") up"
|
||||
;;
|
||||
|
||||
*)
|
||||
fatal "Your system $OSTYPE is not yet supported. We are happy to receive patches to address this :)"
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
frag "
|
||||
# Deploy the classic wireguard private key
|
||||
wg set $(enquote "${dev}") private-key $(enquote "${skdir}/wgsk")"
|
||||
|
||||
|
||||
if test -n "${lport}"; then
|
||||
frag_append "listen-port $(enquote "$(( lport + 1 ))")"
|
||||
fi
|
||||
|
||||
frag "
|
||||
# Launch the post quantum wireguard exchange daemon
|
||||
$(enquote "${binary}") exchange"
|
||||
|
||||
if (( verbose == 1 )); then
|
||||
frag_append "verbose"
|
||||
fi
|
||||
|
||||
frag_append_esc " secret-key $(enquote "${skdir}/pqsk")"
|
||||
frag_append_esc " public-key $(enquote "${skdir}/pqpk")"
|
||||
|
||||
if test -n "${lport}"; then
|
||||
frag_append_esc " listen $(enquote "${lip}:${lport}")"
|
||||
fi
|
||||
|
||||
usagestack+=("peer" "PUBLIC_KEYS_DIR endpoint IP:PORT")
|
||||
|
||||
while (( $# > 0 )); do
|
||||
shift; # Skip "peer" argument
|
||||
|
||||
local peerdir ip port keepalive allowedips
|
||||
peerdir="${1%/}"; shift || fatal "Required peer argument: PUBLIC_KEYS_DIR"
|
||||
|
||||
while (( $# > 0 )); do
|
||||
local arg; arg="$1"; shift
|
||||
case "${arg}" in
|
||||
peer) set -- "peer" "$@"; break;; # Next peer
|
||||
endpoint) ip="${1%:*}"; port="${1##*:}"; shift;;
|
||||
persistent-keepalive) keepalive="${1}"; shift;;
|
||||
allowed-ips) allowedips="${1}"; shift;;
|
||||
-h | -help | --help | help) usage; return 0;;
|
||||
*) fatal "Unknown option ${arg}";;
|
||||
esac
|
||||
done
|
||||
|
||||
# Public key
|
||||
frag_append_esc " peer public-key $(enquote "${peerdir}/pqpk")"
|
||||
|
||||
# PSK
|
||||
local pskfile; pskfile="${peerdir}/psk"
|
||||
if test -f "${pskfile}"; then
|
||||
frag_append_esc " preshared-key $(enquote "${pskfile}")"
|
||||
fi
|
||||
|
||||
|
||||
if test -n "${ip}"; then
|
||||
frag_append_esc " endpoint $(enquote "${ip}:${port}")"
|
||||
fi
|
||||
|
||||
frag_append_esc " wireguard $(enquote "${dev}") $(enquote "$(cat "${peerdir}/wgpk")")"
|
||||
|
||||
if test -n "${ip}"; then
|
||||
frag_append_esc " endpoint $(enquote "${ip}:$(( port + 1 ))")"
|
||||
fi
|
||||
|
||||
if test -n "${keepalive}"; then
|
||||
frag_append_esc " persistent-keepalive $(enquote "${keepalive}")"
|
||||
fi
|
||||
|
||||
if test -n "${allowedips}"; then
|
||||
frag_append_esc " allowed-ips $(enquote "${allowedips}")"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
find_rosenpass_binary() {
|
||||
local binary; binary=""
|
||||
if [[ -n "${gitdir}" ]]; then
|
||||
# If rp is run from the git repo, use the newest build artifact
|
||||
binary=$(
|
||||
find "${gitdir}/result/bin/${project_name}" \
|
||||
"${gitdir}"/target/{release,debug}/"${project_name}" \
|
||||
-printf "%T@ %p\n" 2>/dev/null \
|
||||
| sort -nr \
|
||||
| awk 'NR==1 { print($2) }'
|
||||
)
|
||||
elif [[ -n "${nixdir}" ]]; then
|
||||
# If rp is run from nix, use the nix-installed rosenpass version
|
||||
binary="${nixdir}/bin/${project_name}"
|
||||
fi
|
||||
|
||||
if [[ -z "${binary}" ]]; then
|
||||
binary="${project_name}"
|
||||
fi
|
||||
|
||||
echo "${binary}"
|
||||
}
|
||||
|
||||
main() {
|
||||
formatting_init
|
||||
cleanup_init
|
||||
usage_init
|
||||
frag_init
|
||||
|
||||
project_name="rosenpass"
|
||||
verbose=0
|
||||
scriptdir="$(dirname "${script}")"
|
||||
gitdir="$(detect_git_dir)" || true
|
||||
if [[ -d /nix ]]; then
|
||||
nixdir="$(readlink -f result/bin/rp | grep -Pio '^/nix/store/[^/]+(?=/bin/[^/]+)')" || true
|
||||
fi
|
||||
binary="$(find_rosenpass_binary)"
|
||||
|
||||
# Parse command
|
||||
|
||||
usagestack+=("[explain]" "[verbose]" "genkey|pubkey|exchange" "[ARGS]...")
|
||||
|
||||
local cmd
|
||||
while (( $# > 0 )); do
|
||||
local arg; arg="$1"; shift
|
||||
case "${arg}" in
|
||||
genkey|pubkey|exchange) cmd="${arg}"; break;;
|
||||
explain) explain=1;;
|
||||
verbose) verbose=1;;
|
||||
-h | -help | --help | help) usage; return 0 ;;
|
||||
*) fatal "Unknown command ${arg}";;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "${cmd}" || fatal "No command supplied"
|
||||
usagestack=("${script}")
|
||||
|
||||
# Execute command
|
||||
|
||||
usagestack+=("${cmd}")
|
||||
"${cmd}" "$@"
|
||||
usagestack=("${script}")
|
||||
|
||||
# Apply transaction
|
||||
|
||||
frag_apply
|
||||
}
|
||||
|
||||
script="$0"
|
||||
main "$@"
|
||||
@@ -1,42 +0,0 @@
|
||||
[package]
|
||||
name = "rp"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Build post-quantum-secure VPNs with WireGuard!"
|
||||
homepage = "https://rosenpass.eu/"
|
||||
repository = "https://github.com/rosenpass/rosenpass"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
base64ct = { workspace = true }
|
||||
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
rosenpass = { workspace = true }
|
||||
rosenpass-ciphers = { workspace = true }
|
||||
rosenpass-cipher-traits = { workspace = true }
|
||||
rosenpass-secret-memory = { workspace = true }
|
||||
rosenpass-util = { workspace = true }
|
||||
rosenpass-wireguard-broker = {workspace = true}
|
||||
|
||||
tokio = {workspace = true}
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
ctrlc-async = "3.2"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
genetlink = "0.2"
|
||||
rtnetlink = "0.14"
|
||||
netlink-packet-core = "0.7"
|
||||
netlink-packet-generic = "0.3"
|
||||
netlink-packet-wireguard = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = {workspace = true}
|
||||
stacker = {workspace = true}
|
||||
|
||||
[features]
|
||||
enable_memfd_alloc = []
|
||||
462
rp/src/cli.rs
462
rp/src/cli.rs
@@ -1,462 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{iter::Peekable, net::SocketAddr};
|
||||
|
||||
use crate::exchange::{ExchangeOptions, ExchangePeer};
|
||||
|
||||
pub enum Command {
|
||||
GenKey {
|
||||
private_keys_dir: PathBuf,
|
||||
},
|
||||
PubKey {
|
||||
private_keys_dir: PathBuf,
|
||||
public_keys_dir: PathBuf,
|
||||
},
|
||||
Exchange(ExchangeOptions),
|
||||
Help,
|
||||
}
|
||||
|
||||
enum CommandType {
|
||||
GenKey,
|
||||
PubKey,
|
||||
Exchange,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Cli {
|
||||
pub verbose: bool,
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
fn fatal<T>(note: &str, command: Option<CommandType>) -> Result<T, String> {
|
||||
match command {
|
||||
Some(command) => match command {
|
||||
CommandType::GenKey => Err(format!("{}\nUsage: rp genkey PRIVATE_KEYS_DIR", note)),
|
||||
CommandType::PubKey => Err(format!("{}\nUsage: rp pubkey PRIVATE_KEYS_DIR PUBLIC_KEYS_DIR", note)),
|
||||
CommandType::Exchange => Err(format!("{}\nUsage: rp exchange PRIVATE_KEYS_DIR [dev <device>] [listen <ip>:<port>] [peer PUBLIC_KEYS_DIR [endpoint <ip>:<port>] [persistent-keepalive <interval>] [allowed-ips <ip1>/<cidr1>[,<ip2>/<cidr2>]...]]...", note)),
|
||||
},
|
||||
None => Err(format!("{}\nUsage: rp [verbose] genkey|pubkey|exchange [ARGS]...", note)),
|
||||
}
|
||||
}
|
||||
|
||||
impl ExchangePeer {
|
||||
pub fn parse(args: &mut &mut Peekable<impl Iterator<Item = String>>) -> Result<Self, String> {
|
||||
let mut peer = ExchangePeer::default();
|
||||
|
||||
if let Some(public_keys_dir) = args.next() {
|
||||
peer.public_keys_dir = PathBuf::from(public_keys_dir);
|
||||
} else {
|
||||
return fatal(
|
||||
"Required positional argument: PUBLIC_KEYS_DIR",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(x) = args.peek() {
|
||||
let x = x.as_str();
|
||||
|
||||
// break if next peer is being defined
|
||||
if x == "peer" {
|
||||
break;
|
||||
}
|
||||
|
||||
let x = args.next().unwrap();
|
||||
let x = x.as_str();
|
||||
|
||||
match x {
|
||||
"endpoint" => {
|
||||
if let Some(addr) = args.next() {
|
||||
if let Ok(addr) = addr.parse::<SocketAddr>() {
|
||||
peer.endpoint = Some(addr);
|
||||
} else {
|
||||
return fatal(
|
||||
"invalid parameter for endpoint option",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return fatal(
|
||||
"endpoint option requires parameter",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
}
|
||||
"persistent-keepalive" => {
|
||||
if let Some(ka) = args.next() {
|
||||
if let Ok(ka) = ka.parse::<u32>() {
|
||||
peer.persistent_keepalive = Some(ka);
|
||||
} else {
|
||||
return fatal(
|
||||
"invalid parameter for persistent-keepalive option",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return fatal(
|
||||
"persistent-keepalive option requires parameter",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
}
|
||||
"allowed-ips" => {
|
||||
if let Some(ips) = args.next() {
|
||||
peer.allowed_ips = Some(ips);
|
||||
} else {
|
||||
return fatal(
|
||||
"allowed-ips option requires parameter",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return fatal(
|
||||
&format!("Unknown option {}", x),
|
||||
Some(CommandType::Exchange),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(peer)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExchangeOptions {
|
||||
pub fn parse(mut args: &mut Peekable<impl Iterator<Item = String>>) -> Result<Self, String> {
|
||||
let mut options = ExchangeOptions::default();
|
||||
|
||||
if let Some(private_keys_dir) = args.next() {
|
||||
options.private_keys_dir = PathBuf::from(private_keys_dir);
|
||||
} else {
|
||||
return fatal(
|
||||
"Required positional argument: PRIVATE_KEYS_DIR",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(x) = args.next() {
|
||||
let x = x.as_str();
|
||||
|
||||
match x {
|
||||
"dev" => {
|
||||
if let Some(device) = args.next() {
|
||||
options.dev = Some(device);
|
||||
} else {
|
||||
return fatal("dev option requires parameter", Some(CommandType::Exchange));
|
||||
}
|
||||
}
|
||||
"listen" => {
|
||||
if let Some(addr) = args.next() {
|
||||
if let Ok(addr) = addr.parse::<SocketAddr>() {
|
||||
options.listen = Some(addr);
|
||||
} else {
|
||||
return fatal(
|
||||
"invalid parameter for listen option",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return fatal(
|
||||
"listen option requires parameter",
|
||||
Some(CommandType::Exchange),
|
||||
);
|
||||
}
|
||||
}
|
||||
"peer" => {
|
||||
let peer = ExchangePeer::parse(&mut args)?;
|
||||
options.peers.push(peer);
|
||||
}
|
||||
_ => {
|
||||
return fatal(
|
||||
&format!("Unknown option {}", x),
|
||||
Some(CommandType::Exchange),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(options)
|
||||
}
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub fn parse(mut args: Peekable<impl Iterator<Item = String>>) -> Result<Self, String> {
|
||||
let mut cli = Cli::default();
|
||||
|
||||
let _ = args.next(); // skip executable name
|
||||
|
||||
while let Some(x) = args.next() {
|
||||
let x = x.as_str();
|
||||
|
||||
match x {
|
||||
"verbose" => {
|
||||
cli.verbose = true;
|
||||
}
|
||||
"explain" => {
|
||||
eprintln!("WARN: the explain argument is no longer supported");
|
||||
}
|
||||
"genkey" => {
|
||||
if cli.command.is_some() {
|
||||
return fatal("Too many commands supplied", None);
|
||||
}
|
||||
|
||||
if let Some(private_keys_dir) = args.next() {
|
||||
let private_keys_dir = PathBuf::from(private_keys_dir);
|
||||
|
||||
cli.command = Some(Command::GenKey { private_keys_dir });
|
||||
} else {
|
||||
return fatal(
|
||||
"Required positional argument: PRIVATE_KEYS_DIR",
|
||||
Some(CommandType::GenKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
"pubkey" => {
|
||||
if cli.command.is_some() {
|
||||
return fatal("Too many commands supplied", None);
|
||||
}
|
||||
|
||||
if let Some(private_keys_dir) = args.next() {
|
||||
let private_keys_dir = PathBuf::from(private_keys_dir);
|
||||
|
||||
if let Some(public_keys_dir) = args.next() {
|
||||
let public_keys_dir = PathBuf::from(public_keys_dir);
|
||||
|
||||
cli.command = Some(Command::PubKey {
|
||||
private_keys_dir,
|
||||
public_keys_dir,
|
||||
});
|
||||
} else {
|
||||
return fatal(
|
||||
"Required positional argument: PUBLIC_KEYS_DIR",
|
||||
Some(CommandType::PubKey),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return fatal(
|
||||
"Required positional argument: PRIVATE_KEYS_DIR",
|
||||
Some(CommandType::PubKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
"exchange" => {
|
||||
if cli.command.is_some() {
|
||||
return fatal("Too many commands supplied", None);
|
||||
}
|
||||
|
||||
let options = ExchangeOptions::parse(&mut args)?;
|
||||
cli.command = Some(Command::Exchange(options));
|
||||
}
|
||||
"help" => {
|
||||
cli.command = Some(Command::Help);
|
||||
}
|
||||
_ => return fatal(&format!("Unknown command {}", x), None),
|
||||
};
|
||||
}
|
||||
|
||||
if cli.command.is_none() {
|
||||
return fatal("No command supplied", None);
|
||||
}
|
||||
|
||||
Ok(cli)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{Cli, Command};
|
||||
|
||||
#[inline]
|
||||
fn parse(arr: &[&str]) -> Result<Cli, String> {
|
||||
Cli::parse(arr.iter().map(|x| x.to_string()).peekable())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_err(arr: &[&str]) -> bool {
|
||||
parse(arr).is_err()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_errors() {
|
||||
assert!(parse_err(&["rp"]));
|
||||
assert!(parse_err(&["rp", "verbose"]));
|
||||
assert!(parse_err(&["rp", "thiscommanddoesntexist"]));
|
||||
assert!(parse_err(&[
|
||||
"rp",
|
||||
"thiscommanddoesntexist",
|
||||
"genkey",
|
||||
"./fakedir/"
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genkey_errors() {
|
||||
assert!(parse_err(&["rp", "genkey"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genkey_works() {
|
||||
let cli = parse(&["rp", "genkey", "./fakedir"]);
|
||||
|
||||
assert!(cli.is_ok());
|
||||
let cli = cli.unwrap();
|
||||
|
||||
assert!(!cli.verbose);
|
||||
assert!(matches!(cli.command, Some(Command::GenKey { .. })));
|
||||
|
||||
match cli.command {
|
||||
Some(Command::GenKey { private_keys_dir }) => {
|
||||
assert_eq!(private_keys_dir.to_str().unwrap(), "./fakedir");
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pubkey_errors() {
|
||||
assert!(parse_err(&["rp", "pubkey"]));
|
||||
assert!(parse_err(&["rp", "pubkey", "./fakedir"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pubkey_works() {
|
||||
let cli = parse(&["rp", "pubkey", "./fakedir", "./fakedir2"]);
|
||||
|
||||
assert!(cli.is_ok());
|
||||
let cli = cli.unwrap();
|
||||
|
||||
assert!(!cli.verbose);
|
||||
assert!(matches!(cli.command, Some(Command::PubKey { .. })));
|
||||
|
||||
match cli.command {
|
||||
Some(Command::PubKey {
|
||||
private_keys_dir,
|
||||
public_keys_dir,
|
||||
}) => {
|
||||
assert_eq!(private_keys_dir.to_str().unwrap(), "./fakedir");
|
||||
assert_eq!(public_keys_dir.to_str().unwrap(), "./fakedir2");
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exchange_errors() {
|
||||
assert!(parse_err(&["rp", "exchange"]));
|
||||
assert!(parse_err(&[
|
||||
"rp",
|
||||
"exchange",
|
||||
"./fakedir",
|
||||
"notarealoption"
|
||||
]));
|
||||
assert!(parse_err(&["rp", "exchange", "./fakedir", "listen"]));
|
||||
assert!(parse_err(&[
|
||||
"rp",
|
||||
"exchange",
|
||||
"./fakedir",
|
||||
"listen",
|
||||
"notarealip"
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exchange_works() {
|
||||
let cli = parse(&["rp", "exchange", "./fakedir"]);
|
||||
|
||||
assert!(cli.is_ok());
|
||||
let cli = cli.unwrap();
|
||||
|
||||
assert!(!cli.verbose);
|
||||
assert!(matches!(cli.command, Some(Command::Exchange(_))));
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Exchange(options)) => {
|
||||
assert_eq!(options.private_keys_dir.to_str().unwrap(), "./fakedir");
|
||||
assert!(options.dev.is_none());
|
||||
assert!(options.listen.is_none());
|
||||
assert_eq!(options.peers.len(), 0);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
let cli = parse(&[
|
||||
"rp",
|
||||
"exchange",
|
||||
"./fakedir",
|
||||
"dev",
|
||||
"devname",
|
||||
"listen",
|
||||
"127.0.0.1:1234",
|
||||
]);
|
||||
|
||||
assert!(cli.is_ok());
|
||||
let cli = cli.unwrap();
|
||||
|
||||
assert!(!cli.verbose);
|
||||
assert!(matches!(cli.command, Some(Command::Exchange(_))));
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Exchange(options)) => {
|
||||
assert_eq!(options.private_keys_dir.to_str().unwrap(), "./fakedir");
|
||||
assert_eq!(options.dev, Some("devname".to_string()));
|
||||
assert_eq!(options.listen, Some("127.0.0.1:1234".parse().unwrap()));
|
||||
assert_eq!(options.peers.len(), 0);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
let cli = parse(&[
|
||||
"rp",
|
||||
"exchange",
|
||||
"./fakedir",
|
||||
"dev",
|
||||
"devname",
|
||||
"listen",
|
||||
"127.0.0.1:1234",
|
||||
"peer",
|
||||
"./fakedir2",
|
||||
"endpoint",
|
||||
"127.0.0.1:2345",
|
||||
"persistent-keepalive",
|
||||
"15",
|
||||
"allowed-ips",
|
||||
"123.234.11.0/24,1.1.1.0/24",
|
||||
"peer",
|
||||
"./fakedir3",
|
||||
"endpoint",
|
||||
"127.0.0.1:5432",
|
||||
"persistent-keepalive",
|
||||
"30",
|
||||
]);
|
||||
|
||||
assert!(cli.is_ok());
|
||||
let cli = cli.unwrap();
|
||||
|
||||
assert!(!cli.verbose);
|
||||
assert!(matches!(cli.command, Some(Command::Exchange(_))));
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Exchange(options)) => {
|
||||
assert_eq!(options.private_keys_dir.to_str().unwrap(), "./fakedir");
|
||||
assert_eq!(options.dev, Some("devname".to_string()));
|
||||
assert_eq!(options.listen, Some("127.0.0.1:1234".parse().unwrap()));
|
||||
assert_eq!(options.peers.len(), 2);
|
||||
|
||||
let peer = &options.peers[0];
|
||||
assert_eq!(peer.public_keys_dir.to_str().unwrap(), "./fakedir2");
|
||||
assert_eq!(peer.endpoint, Some("127.0.0.1:2345".parse().unwrap()));
|
||||
assert_eq!(peer.persistent_keepalive, Some(15));
|
||||
assert_eq!(
|
||||
peer.allowed_ips,
|
||||
Some("123.234.11.0/24,1.1.1.0/24".to_string())
|
||||
);
|
||||
|
||||
let peer = &options.peers[1];
|
||||
assert_eq!(peer.public_keys_dir.to_str().unwrap(), "./fakedir3");
|
||||
assert_eq!(peer.endpoint, Some("127.0.0.1:5432".parse().unwrap()));
|
||||
assert_eq!(peer.persistent_keepalive, Some(30));
|
||||
assert!(peer.allowed_ips.is_none());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::key::WG_B64_LEN;
|
||||
#[derive(Default)]
|
||||
pub struct ExchangePeer {
|
||||
pub public_keys_dir: PathBuf,
|
||||
pub endpoint: Option<SocketAddr>,
|
||||
pub persistent_keepalive: Option<u32>,
|
||||
pub allowed_ips: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExchangeOptions {
|
||||
pub verbose: bool,
|
||||
pub private_keys_dir: PathBuf,
|
||||
pub dev: Option<String>,
|
||||
pub listen: Option<SocketAddr>,
|
||||
pub peers: Vec<ExchangePeer>,
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
pub async fn exchange(_: ExchangeOptions) -> Result<()> {
|
||||
use anyhow::anyhow;
|
||||
|
||||
Err(anyhow!(
|
||||
"Your system {} is not yet supported. We are happy to receive patches to address this :)",
|
||||
std::env::consts::OS
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
mod netlink {
|
||||
use anyhow::Result;
|
||||
use futures_util::{StreamExt as _, TryStreamExt as _};
|
||||
use genetlink::GenetlinkHandle;
|
||||
use netlink_packet_core::{NLM_F_ACK, NLM_F_REQUEST};
|
||||
use netlink_packet_wireguard::nlas::WgDeviceAttrs;
|
||||
use rtnetlink::Handle;
|
||||
|
||||
pub async fn link_create_and_up(rtnetlink: &Handle, link_name: String) -> Result<u32> {
|
||||
// add the link
|
||||
rtnetlink
|
||||
.link()
|
||||
.add()
|
||||
.wireguard(link_name.clone())
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// retrieve the link to be able to up it
|
||||
let link = rtnetlink
|
||||
.link()
|
||||
.get()
|
||||
.match_name(link_name.clone())
|
||||
.execute()
|
||||
.into_stream()
|
||||
.into_future()
|
||||
.await
|
||||
.0
|
||||
.unwrap()?;
|
||||
|
||||
// up the link
|
||||
rtnetlink
|
||||
.link()
|
||||
.set(link.header.index)
|
||||
.up()
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
Ok(link.header.index)
|
||||
}
|
||||
|
||||
pub async fn link_cleanup(rtnetlink: &Handle, index: u32) -> Result<()> {
|
||||
rtnetlink.link().del(index).execute().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn link_cleanup_standalone(index: u32) -> Result<()> {
|
||||
let (connection, rtnetlink, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
// We don't care if this fails, as the device may already have been auto-cleaned up.
|
||||
let _ = rtnetlink.link().del(index).execute().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This replicates the functionality of the `wg set` command line tool.
|
||||
///
|
||||
/// It sets the specified WireGuard attributes of the indexed device by
|
||||
/// communicating with WireGuard's generic netlink interface, like the
|
||||
/// `wg` tool does.
|
||||
pub async fn wg_set(
|
||||
genetlink: &mut GenetlinkHandle,
|
||||
index: u32,
|
||||
mut attr: Vec<WgDeviceAttrs>,
|
||||
) -> Result<()> {
|
||||
use futures_util::StreamExt as _;
|
||||
use netlink_packet_core::{NetlinkMessage, NetlinkPayload};
|
||||
use netlink_packet_generic::GenlMessage;
|
||||
use netlink_packet_wireguard::{Wireguard, WireguardCmd};
|
||||
|
||||
// Scope our `set` command to only the device of the specified index
|
||||
attr.insert(0, WgDeviceAttrs::IfIndex(index));
|
||||
|
||||
// Construct the WireGuard-specific netlink packet
|
||||
let wgc = Wireguard {
|
||||
cmd: WireguardCmd::SetDevice,
|
||||
nlas: attr,
|
||||
};
|
||||
|
||||
// Construct final message
|
||||
let genl = GenlMessage::from_payload(wgc);
|
||||
let mut nlmsg = NetlinkMessage::from(genl);
|
||||
nlmsg.header.flags = NLM_F_REQUEST | NLM_F_ACK;
|
||||
|
||||
// Send and wait for the ACK or error
|
||||
let (res, _) = genetlink.request(nlmsg).await?.into_future().await;
|
||||
if let Some(res) = res {
|
||||
let res = res?;
|
||||
if let NetlinkPayload::Error(err) = res.payload {
|
||||
return Err(err.to_io().into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub async fn exchange(options: ExchangeOptions) -> Result<()> {
|
||||
use std::fs;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use netlink_packet_wireguard::{constants::WG_KEY_LEN, nlas::WgDeviceAttrs};
|
||||
use rosenpass::{
|
||||
app_server::{AppServer, BrokerPeer},
|
||||
config::Verbosity,
|
||||
protocol::{SPk, SSk, SymKey},
|
||||
};
|
||||
use rosenpass_secret_memory::Secret;
|
||||
use rosenpass_util::file::{LoadValue as _, LoadValueB64};
|
||||
use rosenpass_wireguard_broker::brokers::native_unix::{
|
||||
NativeUnixBroker, NativeUnixBrokerConfigBaseBuilder, NativeUnixBrokerConfigBaseBuilderError,
|
||||
};
|
||||
|
||||
let (connection, rtnetlink, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let link_name = options.dev.unwrap_or("rosenpass0".to_string());
|
||||
let link_index = netlink::link_create_and_up(&rtnetlink, link_name.clone()).await?;
|
||||
|
||||
ctrlc_async::set_async_handler(async move {
|
||||
netlink::link_cleanup_standalone(link_index)
|
||||
.await
|
||||
.expect("Failed to clean up");
|
||||
})?;
|
||||
|
||||
// Deploy the classic wireguard private key
|
||||
let (connection, mut genetlink, _) = genetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let wgsk_path = options.private_keys_dir.join("wgsk");
|
||||
|
||||
let wgsk = Secret::<WG_KEY_LEN>::load_b64::<WG_B64_LEN, _>(wgsk_path)?;
|
||||
|
||||
let mut attr: Vec<WgDeviceAttrs> = Vec::with_capacity(2);
|
||||
attr.push(WgDeviceAttrs::PrivateKey(*wgsk.secret()));
|
||||
|
||||
if let Some(listen) = options.listen {
|
||||
if listen.port() == u16::MAX {
|
||||
return Err(anyhow!("You may not use {} as the listen port.", u16::MAX));
|
||||
}
|
||||
|
||||
attr.push(WgDeviceAttrs::ListenPort(listen.port() + 1));
|
||||
}
|
||||
|
||||
netlink::wg_set(&mut genetlink, link_index, attr).await?;
|
||||
|
||||
let pqsk = options.private_keys_dir.join("pqsk");
|
||||
let pqpk = options.private_keys_dir.join("pqpk");
|
||||
|
||||
let sk = SSk::load(&pqsk)?;
|
||||
let pk = SPk::load(&pqpk)?;
|
||||
|
||||
let mut srv = Box::new(AppServer::new(
|
||||
sk,
|
||||
pk,
|
||||
if let Some(listen) = options.listen {
|
||||
vec![listen]
|
||||
} else {
|
||||
Vec::with_capacity(0)
|
||||
},
|
||||
if options.verbose {
|
||||
Verbosity::Verbose
|
||||
} else {
|
||||
Verbosity::Quiet
|
||||
},
|
||||
None,
|
||||
)?);
|
||||
|
||||
let broker_store_ptr = srv.register_broker(Box::new(NativeUnixBroker::new()))?;
|
||||
|
||||
fn cfg_err_map(e: NativeUnixBrokerConfigBaseBuilderError) -> anyhow::Error {
|
||||
anyhow::Error::msg(format!("NativeUnixBrokerConfigBaseBuilderError: {:?}", e))
|
||||
}
|
||||
|
||||
for peer in options.peers {
|
||||
let wgpk = peer.public_keys_dir.join("wgpk");
|
||||
let pqpk = peer.public_keys_dir.join("pqpk");
|
||||
let psk = peer.public_keys_dir.join("psk");
|
||||
|
||||
let mut extra_params: Vec<String> = Vec::with_capacity(6);
|
||||
if let Some(endpoint) = peer.endpoint {
|
||||
extra_params.push("endpoint".to_string());
|
||||
|
||||
// Peer endpoints always use (port + 1) in wg set params
|
||||
let endpoint = SocketAddr::new(endpoint.ip(), endpoint.port() + 1);
|
||||
extra_params.push(endpoint.to_string());
|
||||
}
|
||||
if let Some(persistent_keepalive) = peer.persistent_keepalive {
|
||||
extra_params.push("persistent-keepalive".to_string());
|
||||
extra_params.push(persistent_keepalive.to_string());
|
||||
}
|
||||
if let Some(allowed_ips) = &peer.allowed_ips {
|
||||
extra_params.push("allowed-ips".to_string());
|
||||
extra_params.push(allowed_ips.clone());
|
||||
}
|
||||
|
||||
let peer_cfg = NativeUnixBrokerConfigBaseBuilder::default()
|
||||
.peer_id_b64(&fs::read_to_string(wgpk)?)?
|
||||
.interface(link_name.clone())
|
||||
.extra_params_ser(&extra_params)?
|
||||
.build()
|
||||
.map_err(cfg_err_map)?;
|
||||
|
||||
let broker_peer = Some(BrokerPeer::new(
|
||||
broker_store_ptr.clone(),
|
||||
Box::new(peer_cfg),
|
||||
));
|
||||
|
||||
srv.add_peer(
|
||||
if psk.exists() {
|
||||
Some(SymKey::load_b64::<WG_B64_LEN, _>(psk))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.transpose()?,
|
||||
SPk::load(&pqpk)?,
|
||||
None,
|
||||
broker_peer,
|
||||
peer.endpoint.map(|x| x.to_string()),
|
||||
)?;
|
||||
}
|
||||
|
||||
let out = srv.event_loop();
|
||||
|
||||
netlink::link_cleanup(&rtnetlink, link_index).await?;
|
||||
|
||||
match out {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
// Check if the returned error is actually EINTR, in which case, the run actually succeeded.
|
||||
let is_ok = if let Some(e) = e.root_cause().downcast_ref::<std::io::Error>() {
|
||||
matches!(e.kind(), std::io::ErrorKind::Interrupted)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_ok {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
rp/src/key.rs
151
rp/src/key.rs
@@ -1,151 +0,0 @@
|
||||
use std::{
|
||||
fs::{self, DirBuilder},
|
||||
os::unix::fs::{DirBuilderExt, PermissionsExt},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use rosenpass_util::file::{LoadValueB64, StoreValueB64};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use rosenpass::protocol::{SPk, SSk};
|
||||
use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::StaticKem;
|
||||
use rosenpass_secret_memory::{file::StoreSecret as _, Public, Secret};
|
||||
|
||||
pub const WG_B64_LEN: usize = 32 * 5 / 3;
|
||||
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
pub fn genkey(_: &Path) -> Result<()> {
|
||||
Err(anyhow!(
|
||||
"Your system {} is not yet supported. We are happy to receive patches to address this :)",
|
||||
std::env::consts::OS
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn genkey(private_keys_dir: &Path) -> Result<()> {
|
||||
if private_keys_dir.exists() {
|
||||
if fs::metadata(private_keys_dir)?.permissions().mode() != 0o700 {
|
||||
return Err(anyhow!(
|
||||
"Directory {:?} has incorrect permissions: please use 0700 for proper security.",
|
||||
private_keys_dir
|
||||
));
|
||||
}
|
||||
} else {
|
||||
DirBuilder::new()
|
||||
.recursive(true)
|
||||
.mode(0o700)
|
||||
.create(private_keys_dir)?;
|
||||
}
|
||||
|
||||
let wgsk_path = private_keys_dir.join("wgsk");
|
||||
let pqsk_path = private_keys_dir.join("pqsk");
|
||||
let pqpk_path = private_keys_dir.join("pqpk");
|
||||
|
||||
if !wgsk_path.exists() {
|
||||
let wgsk: Secret<32> = Secret::random();
|
||||
wgsk.store_b64::<WG_B64_LEN, _>(wgsk_path)?;
|
||||
} else {
|
||||
eprintln!(
|
||||
"WireGuard secret key already exists at {:#?}: not regenerating",
|
||||
wgsk_path
|
||||
);
|
||||
}
|
||||
|
||||
if !pqsk_path.exists() && !pqpk_path.exists() {
|
||||
let mut pqsk = SSk::random();
|
||||
let mut pqpk = SPk::random();
|
||||
StaticKem::keygen(pqsk.secret_mut(), pqpk.secret_mut())?;
|
||||
pqpk.store_secret(pqpk_path)?;
|
||||
pqsk.store_secret(pqsk_path)?;
|
||||
} else {
|
||||
eprintln!(
|
||||
"Rosenpass keys already exist in {:#?}: not regenerating",
|
||||
private_keys_dir
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pubkey(private_keys_dir: &Path, public_keys_dir: &Path) -> Result<()> {
|
||||
if public_keys_dir.exists() {
|
||||
return Err(anyhow!("Directory {:?} already exists", public_keys_dir));
|
||||
}
|
||||
|
||||
fs::create_dir_all(public_keys_dir)?;
|
||||
|
||||
let private_wgsk = private_keys_dir.join("wgsk");
|
||||
let public_wgpk = public_keys_dir.join("wgpk");
|
||||
let private_pqpk = private_keys_dir.join("pqpk");
|
||||
let public_pqpk = public_keys_dir.join("pqpk");
|
||||
|
||||
let wgsk = Secret::load_b64::<WG_B64_LEN, _>(private_wgsk)?;
|
||||
let mut wgpk: Public<32> = {
|
||||
let mut secret = x25519_dalek::StaticSecret::from(*wgsk.secret());
|
||||
let public = x25519_dalek::PublicKey::from(&secret);
|
||||
secret.zeroize();
|
||||
Public::from_slice(public.as_bytes())
|
||||
};
|
||||
|
||||
wgpk.store_b64::<WG_B64_LEN, _>(public_wgpk)?;
|
||||
wgpk.zeroize();
|
||||
|
||||
fs::copy(private_pqpk, public_pqpk)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use rosenpass::protocol::{SPk, SSk};
|
||||
use rosenpass_secret_memory::secret_policy_try_use_memfd_secrets;
|
||||
use rosenpass_secret_memory::Secret;
|
||||
use rosenpass_util::file::LoadValue;
|
||||
use rosenpass_util::file::LoadValueB64;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::key::{genkey, pubkey, WG_B64_LEN};
|
||||
|
||||
#[test]
|
||||
fn test_key_loopback() {
|
||||
secret_policy_try_use_memfd_secrets();
|
||||
let private_keys_dir = tempdir().unwrap();
|
||||
fs::remove_dir(private_keys_dir.path()).unwrap();
|
||||
|
||||
// Guranteed to have 16MB of stack size
|
||||
stacker::grow(8 * 1024 * 1024, || {
|
||||
assert!(genkey(private_keys_dir.path()).is_ok());
|
||||
});
|
||||
|
||||
assert!(private_keys_dir.path().exists());
|
||||
assert!(private_keys_dir.path().is_dir());
|
||||
assert!(SPk::load(private_keys_dir.path().join("pqpk")).is_ok());
|
||||
assert!(SSk::load(private_keys_dir.path().join("pqsk")).is_ok());
|
||||
assert!(
|
||||
Secret::<32>::load_b64::<WG_B64_LEN, _>(private_keys_dir.path().join("wgsk")).is_ok()
|
||||
);
|
||||
|
||||
let public_keys_dir = tempdir().unwrap();
|
||||
fs::remove_dir(public_keys_dir.path()).unwrap();
|
||||
|
||||
// Guranteed to have 16MB of stack size
|
||||
stacker::grow(8 * 1024 * 1024, || {
|
||||
assert!(pubkey(private_keys_dir.path(), public_keys_dir.path()).is_ok());
|
||||
});
|
||||
|
||||
assert!(public_keys_dir.path().exists());
|
||||
assert!(public_keys_dir.path().is_dir());
|
||||
assert!(SPk::load(public_keys_dir.path().join("pqpk")).is_ok());
|
||||
assert!(
|
||||
Secret::<32>::load_b64::<WG_B64_LEN, _>(public_keys_dir.path().join("wgpk")).is_ok()
|
||||
);
|
||||
|
||||
let pk_1 = fs::read(private_keys_dir.path().join("pqpk")).unwrap();
|
||||
let pk_2 = fs::read(public_keys_dir.path().join("pqpk")).unwrap();
|
||||
assert_eq!(pk_1, pk_2);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use std::process::exit;
|
||||
|
||||
use cli::{Cli, Command};
|
||||
use exchange::exchange;
|
||||
use key::{genkey, pubkey};
|
||||
use rosenpass_secret_memory::policy;
|
||||
|
||||
mod cli;
|
||||
mod exchange;
|
||||
mod key;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[cfg(feature = "enable_memfd_alloc")]
|
||||
policy::secret_policy_try_use_memfd_secrets();
|
||||
#[cfg(not(feature = "enable_memfd_alloc"))]
|
||||
policy::secret_policy_use_only_malloc_secrets();
|
||||
|
||||
let cli = match Cli::parse(std::env::args().peekable()) {
|
||||
Ok(cli) => cli,
|
||||
Err(err) => {
|
||||
eprintln!("{}", err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let command = cli.command.unwrap();
|
||||
|
||||
let res = match command {
|
||||
Command::GenKey { private_keys_dir } => genkey(&private_keys_dir),
|
||||
Command::PubKey {
|
||||
private_keys_dir,
|
||||
public_keys_dir,
|
||||
} => pubkey(&private_keys_dir, &public_keys_dir),
|
||||
Command::Exchange(mut options) => {
|
||||
options.verbose = cli.verbose;
|
||||
exchange(options).await
|
||||
}
|
||||
Command::Help => {
|
||||
println!("Usage: rp [verbose] genkey|pubkey|exchange [ARGS]...");
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
eprintln!("An error occurred: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,3 @@ log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
allocator-api2-tests = { workspace = true }
|
||||
tempfile = {workspace = true}
|
||||
base64ct = {workspace = true}
|
||||
procspawn = {workspace = true}
|
||||
@@ -4,41 +4,37 @@ use std::ptr::NonNull;
|
||||
use allocator_api2::alloc::{AllocError, Allocator, Layout};
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct MallocAllocatorContents;
|
||||
struct MemsecAllocatorContents;
|
||||
|
||||
/// Memory allocation using using the memsec crate
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct MallocAllocator {
|
||||
_dummy_private_data: MallocAllocatorContents,
|
||||
pub struct MemsecAllocator {
|
||||
_dummy_private_data: MemsecAllocatorContents,
|
||||
}
|
||||
|
||||
/// A box backed by the memsec allocator
|
||||
pub type MallocBox<T> = allocator_api2::boxed::Box<T, MallocAllocator>;
|
||||
pub type MemsecBox<T> = allocator_api2::boxed::Box<T, MemsecAllocator>;
|
||||
|
||||
/// A vector backed by the memsec allocator
|
||||
pub type MallocVec<T> = allocator_api2::vec::Vec<T, MallocAllocator>;
|
||||
pub type MemsecVec<T> = allocator_api2::vec::Vec<T, MemsecAllocator>;
|
||||
|
||||
pub fn malloc_box_try<T>(x: T) -> Result<MallocBox<T>, AllocError> {
|
||||
MallocBox::<T>::try_new_in(x, MallocAllocator::new())
|
||||
pub fn memsec_box<T>(x: T) -> MemsecBox<T> {
|
||||
MemsecBox::<T>::new_in(x, MemsecAllocator::new())
|
||||
}
|
||||
|
||||
pub fn malloc_box<T>(x: T) -> MallocBox<T> {
|
||||
MallocBox::<T>::new_in(x, MallocAllocator::new())
|
||||
pub fn memsec_vec<T>() -> MemsecVec<T> {
|
||||
MemsecVec::<T>::new_in(MemsecAllocator::new())
|
||||
}
|
||||
|
||||
pub fn malloc_vec<T>() -> MallocVec<T> {
|
||||
MallocVec::<T>::new_in(MallocAllocator::new())
|
||||
}
|
||||
|
||||
impl MallocAllocator {
|
||||
impl MemsecAllocator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
_dummy_private_data: MallocAllocatorContents,
|
||||
_dummy_private_data: MemsecAllocatorContents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Allocator for MallocAllocator {
|
||||
unsafe impl Allocator for MemsecAllocator {
|
||||
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
|
||||
// Call memsec allocator
|
||||
let mem: Option<NonNull<[u8]>> = unsafe { memsec::malloc_sized(layout.size()) };
|
||||
@@ -52,8 +48,8 @@ unsafe impl Allocator for MallocAllocator {
|
||||
// Ensure the right alignment is used
|
||||
let off = (mem.as_ptr() as *const u8).align_offset(layout.align());
|
||||
if off != 0 {
|
||||
log::error!("Allocation {layout:?} was requested but malloc-based memsec returned allocation \
|
||||
with offset {off} from the requested alignment. Malloc always allocates values \
|
||||
log::error!("Allocation {layout:?} was requested but memsec returned allocation \
|
||||
with offset {off} from the requested alignment. Memsec always allocates values \
|
||||
at the end of a memory page for security reasons, custom alignments are not supported. \
|
||||
You could try allocating an oversized value.");
|
||||
unsafe { memsec::free(mem) };
|
||||
@@ -70,7 +66,7 @@ unsafe impl Allocator for MallocAllocator {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MallocAllocator {
|
||||
impl fmt::Debug for MemsecAllocator {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str("<memsec based Rust allocator>")
|
||||
}
|
||||
@@ -82,21 +78,21 @@ mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
make_test! { test_sizes(MallocAllocator::new()) }
|
||||
make_test! { test_vec(MallocAllocator::new()) }
|
||||
make_test! { test_many_boxes(MallocAllocator::new()) }
|
||||
make_test! { test_sizes(MemsecAllocator::new()) }
|
||||
make_test! { test_vec(MemsecAllocator::new()) }
|
||||
make_test! { test_many_boxes(MemsecAllocator::new()) }
|
||||
|
||||
#[test]
|
||||
fn malloc_allocation() {
|
||||
let alloc = MallocAllocator::new();
|
||||
malloc_allocation_impl::<0>(&alloc);
|
||||
malloc_allocation_impl::<7>(&alloc);
|
||||
malloc_allocation_impl::<8>(&alloc);
|
||||
malloc_allocation_impl::<64>(&alloc);
|
||||
malloc_allocation_impl::<999>(&alloc);
|
||||
fn memsec_allocation() {
|
||||
let alloc = MemsecAllocator::new();
|
||||
memsec_allocation_impl::<0>(&alloc);
|
||||
memsec_allocation_impl::<7>(&alloc);
|
||||
memsec_allocation_impl::<8>(&alloc);
|
||||
memsec_allocation_impl::<64>(&alloc);
|
||||
memsec_allocation_impl::<999>(&alloc);
|
||||
}
|
||||
|
||||
fn malloc_allocation_impl<const N: usize>(alloc: &MallocAllocator) {
|
||||
fn memsec_allocation_impl<const N: usize>(alloc: &MemsecAllocator) {
|
||||
let layout = Layout::new::<[u8; N]>();
|
||||
let mem = alloc.allocate(layout).unwrap();
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
use std::fmt;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use allocator_api2::alloc::{AllocError, Allocator, Layout};
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct MemfdSecAllocatorContents;
|
||||
|
||||
/// Memory allocation using using the memsec crate
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct MemfdSecAllocator {
|
||||
_dummy_private_data: MemfdSecAllocatorContents,
|
||||
}
|
||||
|
||||
/// A box backed by the memsec allocator
|
||||
pub type MemfdSecBox<T> = allocator_api2::boxed::Box<T, MemfdSecAllocator>;
|
||||
|
||||
/// A vector backed by the memsec allocator
|
||||
pub type MemfdSecVec<T> = allocator_api2::vec::Vec<T, MemfdSecAllocator>;
|
||||
|
||||
pub fn memfdsec_box_try<T>(x: T) -> Result<MemfdSecBox<T>, AllocError> {
|
||||
MemfdSecBox::<T>::try_new_in(x, MemfdSecAllocator::new())
|
||||
}
|
||||
|
||||
pub fn memfdsec_box<T>(x: T) -> MemfdSecBox<T> {
|
||||
MemfdSecBox::<T>::new_in(x, MemfdSecAllocator::new())
|
||||
}
|
||||
|
||||
pub fn memfdsec_vec<T>() -> MemfdSecVec<T> {
|
||||
MemfdSecVec::<T>::new_in(MemfdSecAllocator::new())
|
||||
}
|
||||
|
||||
impl MemfdSecAllocator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
_dummy_private_data: MemfdSecAllocatorContents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Allocator for MemfdSecAllocator {
|
||||
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
|
||||
// Call memsec allocator
|
||||
let mem: Option<NonNull<[u8]>> = unsafe { memsec::memfd_secret_sized(layout.size()) };
|
||||
|
||||
// Unwrap the option
|
||||
let Some(mem) = mem else {
|
||||
log::error!("Allocation {layout:?} was requested but memfd-based memsec returned a null pointer");
|
||||
return Err(AllocError);
|
||||
};
|
||||
|
||||
// Ensure the right alignment is used
|
||||
let off = (mem.as_ptr() as *const u8).align_offset(layout.align());
|
||||
if off != 0 {
|
||||
log::error!("Allocation {layout:?} was requested but memfd-based memsec returned allocation \
|
||||
with offset {off} from the requested alignment. Memfd always allocates values \
|
||||
at the end of a memory page for security reasons, custom alignments are not supported. \
|
||||
You could try allocating an oversized value.");
|
||||
unsafe { memsec::free_memfd_secret(mem) };
|
||||
return Err(AllocError);
|
||||
};
|
||||
|
||||
Ok(mem)
|
||||
}
|
||||
|
||||
unsafe fn deallocate(&self, ptr: NonNull<u8>, _layout: Layout) {
|
||||
unsafe {
|
||||
memsec::free_memfd_secret(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MemfdSecAllocator {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str("<memsec based Rust allocator>")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use allocator_api2_tests::make_test;
|
||||
|
||||
use super::*;
|
||||
|
||||
make_test! { test_sizes(MemfdSecAllocator::new()) }
|
||||
make_test! { test_vec(MemfdSecAllocator::new()) }
|
||||
make_test! { test_many_boxes(MemfdSecAllocator::new()) }
|
||||
|
||||
#[test]
|
||||
fn memfdsec_allocation() {
|
||||
let alloc = MemfdSecAllocator::new();
|
||||
memfdsec_allocation_impl::<0>(&alloc);
|
||||
memfdsec_allocation_impl::<7>(&alloc);
|
||||
memfdsec_allocation_impl::<8>(&alloc);
|
||||
memfdsec_allocation_impl::<64>(&alloc);
|
||||
memfdsec_allocation_impl::<999>(&alloc);
|
||||
}
|
||||
|
||||
fn memfdsec_allocation_impl<const N: usize>(alloc: &MemfdSecAllocator) {
|
||||
let layout = Layout::new::<[u8; N]>();
|
||||
let mem = alloc.allocate(layout).unwrap();
|
||||
|
||||
// https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations
|
||||
// promises us that allocated memory is initialized with the magic byte 0xDB
|
||||
// and memsec promises to provide a reimplementation of the libsodium mechanism;
|
||||
// it uses the magic value 0xD0 though
|
||||
assert_eq!(unsafe { mem.as_ref() }, &[0xD0u8; N]);
|
||||
let mem = NonNull::new(mem.as_ptr() as *mut u8).unwrap();
|
||||
unsafe { alloc.deallocate(mem, layout) };
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod malloc;
|
||||
pub mod memfdsec;
|
||||
@@ -1,86 +1,6 @@
|
||||
pub mod memsec;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use allocator_api2::alloc::{AllocError, Allocator};
|
||||
use memsec::malloc::MallocAllocator;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use memsec::memfdsec::MemfdSecAllocator;
|
||||
|
||||
static ALLOC_TYPE: OnceLock<SecretAllocType> = OnceLock::new();
|
||||
|
||||
/// Sets the secret allocation type to use.
|
||||
/// Intended usage at startup before secret allocation
|
||||
/// takes place
|
||||
pub fn set_secret_alloc_type(alloc_type: SecretAllocType) {
|
||||
ALLOC_TYPE.set(alloc_type).unwrap();
|
||||
}
|
||||
|
||||
pub fn get_or_init_secret_alloc_type(alloc_type: SecretAllocType) -> SecretAllocType {
|
||||
*ALLOC_TYPE.get_or_init(|| alloc_type)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SecretAllocType {
|
||||
MemsecMalloc,
|
||||
#[cfg(target_os = "linux")]
|
||||
MemsecMemfdSec,
|
||||
}
|
||||
|
||||
pub struct SecretAlloc {
|
||||
alloc_type: SecretAllocType,
|
||||
}
|
||||
|
||||
impl Default for SecretAlloc {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
alloc_type: *ALLOC_TYPE.get().expect(
|
||||
"Secret security policy not specified. \
|
||||
Run the specifying policy function in \
|
||||
rosenpass_secret_memory::policy or set a \
|
||||
custom policy by initializing \
|
||||
rosenpass_secret_memory::alloc::ALLOC_TYPE \
|
||||
before using secrets",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Allocator for SecretAlloc {
|
||||
fn allocate(
|
||||
&self,
|
||||
layout: std::alloc::Layout,
|
||||
) -> Result<std::ptr::NonNull<[u8]>, allocator_api2::alloc::AllocError> {
|
||||
match self.alloc_type {
|
||||
SecretAllocType::MemsecMalloc => MallocAllocator::default().allocate(layout),
|
||||
#[cfg(target_os = "linux")]
|
||||
SecretAllocType::MemsecMemfdSec => MemfdSecAllocator::default().allocate(layout),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn deallocate(&self, ptr: std::ptr::NonNull<u8>, layout: std::alloc::Layout) {
|
||||
match self.alloc_type {
|
||||
SecretAllocType::MemsecMalloc => MallocAllocator::default().deallocate(ptr, layout),
|
||||
#[cfg(target_os = "linux")]
|
||||
SecretAllocType::MemsecMemfdSec => MemfdSecAllocator::default().deallocate(ptr, layout),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SecretBox<T> = allocator_api2::boxed::Box<T, SecretAlloc>;
|
||||
|
||||
/// A vector backed by the memsec allocator
|
||||
pub type SecretVec<T> = allocator_api2::vec::Vec<T, SecretAlloc>;
|
||||
|
||||
pub fn secret_box_try<T>(x: T) -> Result<SecretBox<T>, AllocError> {
|
||||
SecretBox::<T>::try_new_in(x, SecretAlloc::default())
|
||||
}
|
||||
|
||||
pub fn secret_box<T>(x: T) -> SecretBox<T> {
|
||||
SecretBox::<T>::new_in(x, SecretAlloc::default())
|
||||
}
|
||||
|
||||
pub fn secret_vec<T>() -> SecretVec<T> {
|
||||
SecretVec::<T>::new_in(SecretAlloc::default())
|
||||
}
|
||||
pub use crate::alloc::memsec::{
|
||||
memsec_box as secret_box, memsec_vec as secret_vec, MemsecAllocator as SecretAllocator,
|
||||
MemsecBox as SecretBox, MemsecVec as SecretVec,
|
||||
};
|
||||
|
||||
@@ -4,5 +4,4 @@ pub trait StoreSecret {
|
||||
type Error;
|
||||
|
||||
fn store_secret<P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>;
|
||||
fn store<P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,3 @@ pub use crate::public::Public;
|
||||
|
||||
mod secret;
|
||||
pub use crate::secret::Secret;
|
||||
|
||||
pub mod policy;
|
||||
pub use crate::policy::*;
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
pub fn secret_policy_try_use_memfd_secrets() {
|
||||
let alloc_type = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if crate::alloc::memsec::memfdsec::memfdsec_box_try(0u8).is_ok() {
|
||||
crate::alloc::SecretAllocType::MemsecMemfdSec
|
||||
} else {
|
||||
crate::alloc::SecretAllocType::MemsecMalloc
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
crate::alloc::SecretAllocType::MemsecMalloc
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
alloc_type,
|
||||
crate::alloc::get_or_init_secret_alloc_type(alloc_type)
|
||||
);
|
||||
|
||||
log::info!("Secrets will be allocated using {:?}", alloc_type);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn secret_policy_use_only_memfd_secrets() {
|
||||
let alloc_type = crate::alloc::SecretAllocType::MemsecMemfdSec;
|
||||
|
||||
assert_eq!(
|
||||
alloc_type,
|
||||
crate::alloc::get_or_init_secret_alloc_type(alloc_type)
|
||||
);
|
||||
|
||||
log::info!("Secrets will be allocated using {:?}", alloc_type);
|
||||
}
|
||||
|
||||
pub fn secret_policy_use_only_malloc_secrets() {
|
||||
let alloc_type = crate::alloc::SecretAllocType::MemsecMalloc;
|
||||
assert_eq!(
|
||||
alloc_type,
|
||||
crate::alloc::get_or_init_secret_alloc_type(alloc_type)
|
||||
);
|
||||
|
||||
log::info!("Secrets will be allocated using {:?}", alloc_type);
|
||||
}
|
||||
|
||||
pub mod test {
|
||||
#[macro_export]
|
||||
macro_rules! test_spawn_process_with_policies {
|
||||
($body:block, $($f: expr),*) => {
|
||||
$(
|
||||
let handle = procspawn::spawn((), |_| {
|
||||
|
||||
$f();
|
||||
|
||||
$body
|
||||
|
||||
});
|
||||
handle.join().unwrap();
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! test_spawn_process_provided_policies {
|
||||
($body: block) => {
|
||||
$crate::test_spawn_process_with_policies!(
|
||||
$body,
|
||||
$crate::policy::secret_policy_try_use_memfd_secrets,
|
||||
$crate::secret_policy_use_only_malloc_secrets
|
||||
);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
$crate::test_spawn_process_with_policies!(
|
||||
$body,
|
||||
$crate::policy::secret_policy_use_only_memfd_secrets
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
use crate::debug::debug_crypto_array;
|
||||
use anyhow::Context;
|
||||
use rand::{Fill as Randomize, Rng};
|
||||
use rosenpass_to::{ops::copy_slice, To};
|
||||
use rosenpass_util::b64::{b64_decode, b64_encode};
|
||||
use rosenpass_util::file::{
|
||||
fopen_r, fopen_w, LoadValue, LoadValueB64, ReadExactToEnd, ReadSliceToEnd, StoreValue,
|
||||
StoreValueB64, StoreValueB64Writer, Visibility,
|
||||
};
|
||||
use rosenpass_util::file::{fopen_r, LoadValue, ReadExactToEnd, StoreValue};
|
||||
use rosenpass_util::functional::mutating;
|
||||
use std::borrow::{Borrow, BorrowMut};
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -116,164 +110,3 @@ impl<const N: usize> StoreValue for Public<N> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> LoadValueB64 for Public<N> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut f = [0u8; F];
|
||||
let mut v = Public::zero();
|
||||
let p = path.as_ref();
|
||||
|
||||
let len = fopen_r(p)?
|
||||
.read_slice_to_end(&mut f)
|
||||
.with_context(|| format!("Could not load file {p:?}"))?;
|
||||
|
||||
b64_decode(&f[0..len], &mut v.value)
|
||||
.with_context(|| format!("Could not decode base64 file {p:?}"))?;
|
||||
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> StoreValueB64 for Public<N> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
|
||||
let p = path.as_ref();
|
||||
let mut f = [0u8; F];
|
||||
let encoded_str = b64_encode(&self.value, &mut f)
|
||||
.with_context(|| format!("Could not encode base64 file {p:?}"))?;
|
||||
fopen_w(p, Visibility::Public)?
|
||||
.write_all(encoded_str.as_bytes())
|
||||
.with_context(|| format!("Could not write file {p:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> StoreValueB64Writer for Public<N> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn store_b64_writer<const F: usize, W: std::io::Write>(
|
||||
&self,
|
||||
mut writer: W,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut f = [0u8; F];
|
||||
let encoded_str =
|
||||
b64_encode(&self.value, &mut f).with_context(|| "Could not encode secret to base64")?;
|
||||
|
||||
writer
|
||||
.write_all(encoded_str.as_bytes())
|
||||
.with_context(|| "Could not write base64 to writer")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Public;
|
||||
use rosenpass_util::{
|
||||
b64::b64_encode,
|
||||
file::{
|
||||
fopen_w, LoadValue, LoadValueB64, StoreValue, StoreValueB64, StoreValueB64Writer,
|
||||
Visibility,
|
||||
},
|
||||
};
|
||||
use std::{fs, os::unix::fs::PermissionsExt};
|
||||
use tempfile::tempdir;
|
||||
|
||||
/// test loading a public from an example file, and then storing it again in a different file
|
||||
#[test]
|
||||
fn test_public_load_store() {
|
||||
const N: usize = 100;
|
||||
|
||||
// Generate original random bytes
|
||||
let original_bytes: [u8; N] = [rand::random(); N];
|
||||
|
||||
// Create a temporary directory
|
||||
let temp_dir = tempdir().unwrap();
|
||||
|
||||
// Store the original public to an example file in the temporary directory
|
||||
let example_file = temp_dir.path().join("example_file");
|
||||
std::fs::write(example_file.clone(), &original_bytes).unwrap();
|
||||
|
||||
// Load the public from the example file
|
||||
|
||||
let loaded_public = Public::load(&example_file).unwrap();
|
||||
|
||||
// Check that the loaded public matches the original bytes
|
||||
assert_eq!(&loaded_public.value, &original_bytes);
|
||||
|
||||
// Store the loaded public to a different file in the temporary directory
|
||||
let new_file = temp_dir.path().join("new_file");
|
||||
loaded_public.store(&new_file).unwrap();
|
||||
|
||||
// Read the contents of the new file
|
||||
let new_file_contents = fs::read(&new_file).unwrap();
|
||||
|
||||
// Read the contents of the original file
|
||||
let original_file_contents = fs::read(&example_file).unwrap();
|
||||
|
||||
// Check that the contents of the new file match the original file
|
||||
assert_eq!(new_file_contents, original_file_contents);
|
||||
}
|
||||
|
||||
/// test loading a base64 encoded public from an example file, and then storing it again in a different file
|
||||
#[test]
|
||||
fn test_public_load_store_base64() {
|
||||
const N: usize = 100;
|
||||
// Generate original random bytes
|
||||
let original_bytes: [u8; N] = [rand::random(); N];
|
||||
// Create a temporary directory
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let example_file = temp_dir.path().join("example_file");
|
||||
let mut encoded_public = [0u8; N * 2];
|
||||
let encoded_public = b64_encode(&original_bytes, &mut encoded_public).unwrap();
|
||||
std::fs::write(&example_file, encoded_public).unwrap();
|
||||
|
||||
// Load the public from the example file
|
||||
let loaded_public = Public::load_b64::<{ N * 2 }, _>(&example_file).unwrap();
|
||||
// Check that the loaded public matches the original bytes
|
||||
assert_eq!(&loaded_public.value, &original_bytes);
|
||||
|
||||
// Store the loaded public to a different file in the temporary directory
|
||||
let new_file = temp_dir.path().join("new_file");
|
||||
loaded_public.store_b64::<{ N * 2 }, _>(&new_file).unwrap();
|
||||
|
||||
// Read the contents of the new file
|
||||
let new_file_contents = fs::read(&new_file).unwrap();
|
||||
// Read the contents of the original file
|
||||
let original_file_contents = fs::read(&example_file).unwrap();
|
||||
// Check that the contents of the new file match the original file
|
||||
assert_eq!(new_file_contents, original_file_contents);
|
||||
|
||||
//Check new file permissions are public
|
||||
let metadata = fs::metadata(&new_file).unwrap();
|
||||
assert_eq!(metadata.permissions().mode() & 0o000777, 0o644);
|
||||
|
||||
// Store the loaded public to a different file in the temporary directory for a second time
|
||||
let new_file = temp_dir.path().join("new_file_writer");
|
||||
let new_file_writer = fopen_w(new_file.clone(), Visibility::Public).unwrap();
|
||||
loaded_public
|
||||
.store_b64_writer::<{ N * 2 }, _>(&new_file_writer)
|
||||
.unwrap();
|
||||
|
||||
// Read the contents of the new file
|
||||
let new_file_contents = fs::read(&new_file).unwrap();
|
||||
// Read the contents of the original file
|
||||
let original_file_contents = fs::read(&example_file).unwrap();
|
||||
// Check that the contents of the new file match the original file
|
||||
assert_eq!(new_file_contents, original_file_contents);
|
||||
|
||||
//Check new file permissions are public
|
||||
let metadata = fs::metadata(&new_file).unwrap();
|
||||
assert_eq!(metadata.permissions().mode() & 0o000777, 0o644);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,18 +9,13 @@ use anyhow::Context;
|
||||
use rand::{Fill as Randomize, Rng};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use rosenpass_util::b64::{b64_decode, b64_encode};
|
||||
use rosenpass_util::file::{
|
||||
fopen_r, LoadValue, LoadValueB64, ReadExactToEnd, ReadSliceToEnd, StoreValueB64,
|
||||
StoreValueB64Writer,
|
||||
};
|
||||
use rosenpass_util::b64::b64_reader;
|
||||
use rosenpass_util::file::{fopen_r, LoadValue, LoadValueB64, ReadExactToEnd};
|
||||
use rosenpass_util::functional::mutating;
|
||||
|
||||
use crate::alloc::{secret_box, SecretBox, SecretVec};
|
||||
use crate::file::StoreSecret;
|
||||
|
||||
use rosenpass_util::file::{fopen_w, Visibility};
|
||||
use std::io::Write;
|
||||
// This might become a problem in library usage; it's effectively a memory
|
||||
// leak which probably isn't a problem right now because most memory will
|
||||
// be reused…
|
||||
@@ -254,214 +249,73 @@ impl<const N: usize> LoadValue for Secret<N> {
|
||||
impl<const N: usize> LoadValueB64 for Secret<N> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
|
||||
let mut f: Secret<F> = Secret::random();
|
||||
fn load_b64<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
|
||||
use std::io::Read;
|
||||
|
||||
let mut v = Self::random();
|
||||
let p = path.as_ref();
|
||||
|
||||
let len = fopen_r(p)?
|
||||
.read_slice_to_end(f.secret_mut())
|
||||
.with_context(|| format!("Could not load file {p:?}"))?;
|
||||
|
||||
b64_decode(&f.secret()[0..len], v.secret_mut())
|
||||
.with_context(|| format!("Could not decode base64 file {p:?}"))?;
|
||||
|
||||
// This might leave some fragments of the secret on the stack;
|
||||
// in practice this is likely not a problem because the stack likely
|
||||
// will be overwritten by something else soon but this is not exactly
|
||||
// guaranteed. It would be possible to remedy this, but since the secret
|
||||
// data will linger in the Linux page cache anyways with the current
|
||||
// implementation, going to great length to erase the secret here is
|
||||
// not worth it right now.
|
||||
b64_reader(&mut fopen_r(p)?)
|
||||
.read_exact(v.secret_mut())
|
||||
.with_context(|| format!("Could not load base64 file {p:?}"))?;
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> StoreValueB64 for Secret<N> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
|
||||
let p = path.as_ref();
|
||||
|
||||
let mut f: Secret<F> = Secret::random();
|
||||
let encoded_str = b64_encode(self.secret(), f.secret_mut())
|
||||
.with_context(|| format!("Could not encode base64 file {p:?}"))?;
|
||||
|
||||
fopen_w(p, Visibility::Secret)?
|
||||
.write_all(encoded_str.as_bytes())
|
||||
.with_context(|| format!("Could not write file {p:?}"))?;
|
||||
f.zeroize();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> StoreValueB64Writer for Secret<N> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn store_b64_writer<const F: usize, W: Write>(&self, mut writer: W) -> anyhow::Result<()> {
|
||||
let mut f: Secret<F> = Secret::random();
|
||||
let encoded_str = b64_encode(self.secret(), f.secret_mut())
|
||||
.with_context(|| "Could not encode secret to base64")?;
|
||||
|
||||
writer
|
||||
.write_all(encoded_str.as_bytes())
|
||||
.with_context(|| "Could not write base64 to writer")?;
|
||||
f.zeroize();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> StoreSecret for Secret<N> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn store_secret<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
|
||||
fopen_w(path, Visibility::Secret)?.write_all(self.secret())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn store<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
|
||||
fopen_w(path, Visibility::Public)?.write_all(self.secret())?;
|
||||
std::fs::write(path, self.secret())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::test_spawn_process_provided_policies;
|
||||
|
||||
use super::*;
|
||||
use std::{fs, os::unix::fs::PermissionsExt};
|
||||
use tempfile::tempdir;
|
||||
|
||||
procspawn::enable_test_support!();
|
||||
|
||||
/// check that we can alloc using the magic pool
|
||||
#[test]
|
||||
fn secret_memory_pool_take() {
|
||||
test_spawn_process_provided_policies!({
|
||||
const N: usize = 0x100;
|
||||
let mut pool = SecretMemoryPool::new();
|
||||
let secret: ZeroizingSecretBox<[u8; N]> = pool.take();
|
||||
assert_eq!(secret.as_ref(), &[0; N]);
|
||||
});
|
||||
const N: usize = 0x100;
|
||||
let mut pool = SecretMemoryPool::new();
|
||||
let secret: ZeroizingSecretBox<[u8; N]> = pool.take();
|
||||
assert_eq!(secret.as_ref(), &[0; N]);
|
||||
}
|
||||
|
||||
/// check that a secret lives, even if its [SecretMemoryPool] is deleted
|
||||
/// check that a secrete lives, even if its [SecretMemoryPool] is deleted
|
||||
#[test]
|
||||
fn secret_memory_pool_drop() {
|
||||
test_spawn_process_provided_policies!({
|
||||
const N: usize = 0x100;
|
||||
let mut pool = SecretMemoryPool::new();
|
||||
let secret: ZeroizingSecretBox<[u8; N]> = pool.take();
|
||||
std::mem::drop(pool);
|
||||
assert_eq!(secret.as_ref(), &[0; N]);
|
||||
});
|
||||
const N: usize = 0x100;
|
||||
let mut pool = SecretMemoryPool::new();
|
||||
let secret: ZeroizingSecretBox<[u8; N]> = pool.take();
|
||||
std::mem::drop(pool);
|
||||
assert_eq!(secret.as_ref(), &[0; N]);
|
||||
}
|
||||
|
||||
/// check that a secret can be reborn, freshly initialized with zero
|
||||
/// check that a secrete can be reborn, freshly initialized with zero
|
||||
#[test]
|
||||
fn secret_memory_pool_release() {
|
||||
test_spawn_process_provided_policies!({
|
||||
const N: usize = 1;
|
||||
let mut pool = SecretMemoryPool::new();
|
||||
let mut secret: ZeroizingSecretBox<[u8; N]> = pool.take();
|
||||
let old_secret_ptr = secret.as_ref().as_ptr();
|
||||
const N: usize = 1;
|
||||
let mut pool = SecretMemoryPool::new();
|
||||
let mut secret: ZeroizingSecretBox<[u8; N]> = pool.take();
|
||||
let old_secret_ptr = secret.as_ref().as_ptr();
|
||||
|
||||
secret.as_mut()[0] = 0x13;
|
||||
pool.release(secret);
|
||||
secret.as_mut()[0] = 0x13;
|
||||
pool.release(secret);
|
||||
|
||||
// now check that we get the same ptr
|
||||
let new_secret: ZeroizingSecretBox<[u8; N]> = pool.take();
|
||||
assert_eq!(old_secret_ptr, new_secret.as_ref().as_ptr());
|
||||
// now check that we get the same ptr
|
||||
let new_secret: ZeroizingSecretBox<[u8; N]> = pool.take();
|
||||
assert_eq!(old_secret_ptr, new_secret.as_ref().as_ptr());
|
||||
|
||||
// and that the secret was zeroized
|
||||
assert_eq!(new_secret.as_ref(), &[0; N]);
|
||||
});
|
||||
}
|
||||
|
||||
/// test loading a secret from an example file, and then storing it again in a different file
|
||||
#[test]
|
||||
fn test_secret_load_store() {
|
||||
test_spawn_process_provided_policies!({
|
||||
const N: usize = 100;
|
||||
|
||||
// Generate original random bytes
|
||||
let original_bytes: [u8; N] = [rand::random(); N];
|
||||
|
||||
// Create a temporary directory
|
||||
let temp_dir = tempdir().unwrap();
|
||||
|
||||
// Store the original secret to an example file in the temporary directory
|
||||
let example_file = temp_dir.path().join("example_file");
|
||||
std::fs::write(example_file.clone(), &original_bytes).unwrap();
|
||||
|
||||
// Load the secret from the example file
|
||||
let loaded_secret = Secret::load(&example_file).unwrap();
|
||||
|
||||
// Check that the loaded secret matches the original bytes
|
||||
assert_eq!(loaded_secret.secret(), &original_bytes);
|
||||
|
||||
// Store the loaded secret to a different file in the temporary directory
|
||||
let new_file = temp_dir.path().join("new_file");
|
||||
loaded_secret.store(&new_file).unwrap();
|
||||
|
||||
// Read the contents of the new file
|
||||
let new_file_contents = fs::read(&new_file).unwrap();
|
||||
|
||||
// Read the contents of the original file
|
||||
let original_file_contents = fs::read(&example_file).unwrap();
|
||||
|
||||
// Check that the contents of the new file match the original file
|
||||
assert_eq!(new_file_contents, original_file_contents);
|
||||
});
|
||||
}
|
||||
|
||||
/// test loading a base64 encoded secret from an example file, and then storing it again in a different file
|
||||
#[test]
|
||||
fn test_secret_load_store_base64() {
|
||||
test_spawn_process_provided_policies!({
|
||||
const N: usize = 100;
|
||||
// Generate original random bytes
|
||||
let original_bytes: [u8; N] = [rand::random(); N];
|
||||
// Create a temporary directory
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let example_file = temp_dir.path().join("example_file");
|
||||
let mut encoded_secret = [0u8; N * 2];
|
||||
let encoded_secret = b64_encode(&original_bytes, &mut encoded_secret).unwrap();
|
||||
|
||||
std::fs::write(&example_file, encoded_secret).unwrap();
|
||||
|
||||
// Load the secret from the example file
|
||||
let loaded_secret = Secret::load_b64::<{ N * 2 }, _>(&example_file).unwrap();
|
||||
// Check that the loaded secret matches the original bytes
|
||||
assert_eq!(loaded_secret.secret(), &original_bytes);
|
||||
|
||||
// Store the loaded secret to a different file in the temporary directory
|
||||
let new_file = temp_dir.path().join("new_file");
|
||||
loaded_secret.store_b64::<{ N * 2 }, _>(&new_file).unwrap();
|
||||
|
||||
// Read the contents of the new file
|
||||
let new_file_contents = fs::read(&new_file).unwrap();
|
||||
// Read the contents of the original file
|
||||
let original_file_contents = fs::read(&example_file).unwrap();
|
||||
// Check that the contents of the new file match the original file
|
||||
assert_eq!(new_file_contents, original_file_contents);
|
||||
|
||||
//Check new file permissions are secret
|
||||
let metadata = fs::metadata(&new_file).unwrap();
|
||||
assert_eq!(metadata.permissions().mode() & 0o000777, 0o600);
|
||||
|
||||
// Store the loaded secret to a different file in the temporary directory for a second time
|
||||
let new_file = temp_dir.path().join("new_file_writer");
|
||||
let new_file_writer = fopen_w(new_file.clone(), Visibility::Secret).unwrap();
|
||||
loaded_secret
|
||||
.store_b64_writer::<{ N * 2 }, _>(&new_file_writer)
|
||||
.unwrap();
|
||||
|
||||
// Read the contents of the new file
|
||||
let new_file_contents = fs::read(&new_file).unwrap();
|
||||
// Read the contents of the original file
|
||||
let original_file_contents = fs::read(&example_file).unwrap();
|
||||
// Check that the contents of the new file match the original file
|
||||
assert_eq!(new_file_contents, original_file_contents);
|
||||
|
||||
//Check new file permissions are secret
|
||||
let metadata = fs::metadata(&new_file).unwrap();
|
||||
assert_eq!(metadata.permissions().mode() & 0o000777, 0o600);
|
||||
});
|
||||
// and that the secret was zeroized
|
||||
assert_eq!(new_secret.as_ref(), &[0; N]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ readme = "readme.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
base64ct = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
rustix = { workspace = true }
|
||||
typenum = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
rustix = {workspace = true}
|
||||
zeroize = {workspace = true}
|
||||
|
||||
136
util/src/b64.rs
136
util/src/b64.rs
@@ -1,130 +1,20 @@
|
||||
use base64ct::{Base64, Decoder as B64Reader, Encoder as B64Writer};
|
||||
use zeroize::Zeroize;
|
||||
use base64::{
|
||||
display::Base64Display as B64Display, read::DecoderReader as B64Reader,
|
||||
write::EncoderWriter as B64Writer,
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use std::fmt::Display;
|
||||
use base64::engine::general_purpose::GeneralPurpose as Base64Engine;
|
||||
const B64ENGINE: Base64Engine = base64::engine::general_purpose::STANDARD;
|
||||
|
||||
pub struct B64DisplayHelper<'a, const F: usize>(&'a [u8]);
|
||||
|
||||
impl<const F: usize> Display for B64DisplayHelper<'_, F> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut bytes = [0u8; F];
|
||||
let string = b64_encode(self.0, &mut bytes).map_err(|_| std::fmt::Error)?;
|
||||
let result = f.write_str(string);
|
||||
bytes.zeroize();
|
||||
result
|
||||
}
|
||||
pub fn fmt_b64<'a>(payload: &'a [u8]) -> B64Display<'a, 'static, Base64Engine> {
|
||||
B64Display::<'a, 'static>::new(payload, &B64ENGINE)
|
||||
}
|
||||
|
||||
pub trait B64Display {
|
||||
fn fmt_b64<const F: usize>(&self) -> B64DisplayHelper<F>;
|
||||
pub fn b64_writer<W: Write>(w: W) -> B64Writer<'static, Base64Engine, W> {
|
||||
B64Writer::new(w, &B64ENGINE)
|
||||
}
|
||||
|
||||
impl B64Display for [u8] {
|
||||
fn fmt_b64<const F: usize>(&self) -> B64DisplayHelper<F> {
|
||||
B64DisplayHelper(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> B64Display for T {
|
||||
fn fmt_b64<const F: usize>(&self) -> B64DisplayHelper<F> {
|
||||
B64DisplayHelper(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn b64_decode(input: &[u8], output: &mut [u8]) -> anyhow::Result<()> {
|
||||
let mut reader = B64Reader::<Base64>::new(input).map_err(|e| anyhow::anyhow!(e))?;
|
||||
match reader.decode(output) {
|
||||
Ok(_) => (),
|
||||
Err(base64ct::Error::InvalidLength) => (),
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!(e));
|
||||
}
|
||||
}
|
||||
if reader.is_finished() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Input not decoded completely (buffer size too small?)"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn b64_encode<'o>(input: &[u8], output: &'o mut [u8]) -> anyhow::Result<&'o str> {
|
||||
let mut writer = B64Writer::<Base64>::new(output).map_err(|e| anyhow::anyhow!(e))?;
|
||||
writer.encode(input).map_err(|e| anyhow::anyhow!(e))?;
|
||||
writer.finish().map_err(|e| anyhow::anyhow!(e))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_b64_encode() {
|
||||
let input = b"Hello, World!";
|
||||
let mut output = [0u8; 20];
|
||||
let result = b64_encode(input, &mut output);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "SGVsbG8sIFdvcmxkIQ==");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_b64_encode_small_buffer() {
|
||||
let input = b"Hello, World!";
|
||||
let mut output = [0u8; 10]; // Small output buffer
|
||||
let result = b64_encode(input, &mut output);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().to_string(), "invalid Base64 length");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_b64_encode_empty_buffer() {
|
||||
let input = b"";
|
||||
let mut output = [0u8; 16];
|
||||
let result = b64_encode(input, &mut output);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_b64_decode() {
|
||||
let input = b"SGVsbG8sIFdvcmxkIQ==";
|
||||
let mut output = [0u8; 1000];
|
||||
b64_decode(input, &mut output).unwrap();
|
||||
assert_eq!(&output[..13], b"Hello, World!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_b64_decode_small_buffer() {
|
||||
let input = b"SGVsbG8sIFdvcmxkIQ==";
|
||||
let mut output = [0u8; 10]; // Small output buffer
|
||||
let result = b64_decode(input, &mut output);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"Input not decoded completely (buffer size too small?)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_b64_decode_empty_buffer() {
|
||||
let input = b"";
|
||||
let mut output = [0u8; 16];
|
||||
let result = b64_decode(input, &mut output);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fmt_b64() {
|
||||
let input = b"Hello, World!";
|
||||
let result = input.fmt_b64::<20>().to_string();
|
||||
assert_eq!(result, "SGVsbG8sIFdvcmxkIQ==");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fmt_b64_empty_input() {
|
||||
let input = b"";
|
||||
let result = input.fmt_b64::<16>().to_string();
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
pub fn b64_reader<R: Read>(r: R) -> B64Reader<'static, Base64Engine, R> {
|
||||
B64Reader::new(r, &B64ENGINE)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
use anyhow::ensure;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::result::Result;
|
||||
use std::{fs::OpenOptions, path::Path};
|
||||
|
||||
pub enum Visibility {
|
||||
Public,
|
||||
Secret,
|
||||
}
|
||||
|
||||
/// Open a file writable
|
||||
pub fn fopen_w<P: AsRef<Path>>(path: P, visibility: Visibility) -> std::io::Result<File> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.create(true).write(true).read(false).truncate(true);
|
||||
match visibility {
|
||||
Visibility::Public => options.mode(0o644),
|
||||
Visibility::Secret => options.mode(0o600),
|
||||
};
|
||||
options.open(path)
|
||||
pub fn fopen_w<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
|
||||
OpenOptions::new()
|
||||
.read(false)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
}
|
||||
/// Open a file readable
|
||||
pub fn fopen_r<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
|
||||
@@ -30,30 +23,6 @@ pub fn fopen_r<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
|
||||
.open(path)
|
||||
}
|
||||
|
||||
pub trait ReadSliceToEnd {
|
||||
type Error;
|
||||
|
||||
fn read_slice_to_end(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error>;
|
||||
}
|
||||
|
||||
impl<R: Read> ReadSliceToEnd for R {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn read_slice_to_end(&mut self, buf: &mut [u8]) -> anyhow::Result<usize> {
|
||||
let mut dummy = [0u8; 8];
|
||||
let mut read = 0;
|
||||
while read < buf.len() {
|
||||
let bytes_read = self.read(&mut buf[read..])?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
read += bytes_read;
|
||||
}
|
||||
ensure!(self.read(&mut dummy)? == 0, "File too long!");
|
||||
Ok(read)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ReadExactToEnd {
|
||||
type Error;
|
||||
|
||||
@@ -82,36 +51,13 @@ pub trait LoadValue {
|
||||
pub trait LoadValueB64 {
|
||||
type Error;
|
||||
|
||||
fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
|
||||
fn load_b64<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait StoreValueB64 {
|
||||
type Error;
|
||||
|
||||
fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait StoreValueB64Writer {
|
||||
type Error;
|
||||
|
||||
fn store_b64_writer<const F: usize, W: std::io::Write>(
|
||||
&self,
|
||||
writer: W,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait StoreValue {
|
||||
type Error;
|
||||
|
||||
fn store<P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait DisplayValueB64 {
|
||||
type Error;
|
||||
|
||||
fn display_b64<'o>(&self, output: &'o mut [u8]) -> Result<&'o str, Self::Error>;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ where
|
||||
const VALUE: Ret = <ConstApplyNegSign<Ret, Unsigned> as IntoConst<Ret>>::VALUE;
|
||||
}
|
||||
|
||||
#[allow(clippy::identity_op)]
|
||||
mod test {
|
||||
use static_assertions::const_assert_eq;
|
||||
use typenum::consts::*;
|
||||
|
||||
@@ -11,45 +11,32 @@ readme = "readme.md"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
zerocopy = { workspace = true }
|
||||
rosenpass-secret-memory = {workspace = true}
|
||||
rosenpass-lenses = { workspace = true }
|
||||
paste = { workspace = true } # TODO: Using lenses should not necessitate importing paste
|
||||
|
||||
# Privileged only
|
||||
wireguard-uapi = { workspace = true }
|
||||
|
||||
# Socket handler only
|
||||
rosenpass-to = { workspace = true }
|
||||
tokio = { version = "1.38.0", features = ["sync", "full", "mio"] }
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
log = { workspace = true }
|
||||
derive_builder = {workspace = true}
|
||||
postcard = {workspace = true}
|
||||
|
||||
# Mio broker client
|
||||
mio = { workspace = true }
|
||||
rosenpass-util = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = {workspace = true}
|
||||
procspawn = {workspace = true}
|
||||
|
||||
[features]
|
||||
enable_broker_api=[]
|
||||
|
||||
[[bin]]
|
||||
name = "rosenpass-wireguard-broker-privileged"
|
||||
path = "src/bin/priviledged.rs"
|
||||
test = false
|
||||
doc = false
|
||||
required-features=["enable_broker_api"]
|
||||
cfg = { target_os = "linux" }
|
||||
|
||||
[[bin]]
|
||||
name = "rosenpass-wireguard-broker-socket-handler"
|
||||
test = false
|
||||
path = "src/bin/socket_handler.rs"
|
||||
doc = false
|
||||
required-features=["enable_broker_api"]
|
||||
cfg = { target_os = "linux" }
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
use std::{borrow::BorrowMut, fmt::Debug};
|
||||
use std::{borrow::BorrowMut, marker::PhantomData};
|
||||
|
||||
use rosenpass_lenses::LenseView;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
config::NetworkBrokerConfig,
|
||||
msgs::{self, REQUEST_MSG_BUFFER_SIZE},
|
||||
},
|
||||
SerializedBrokerConfig, WireGuardBroker,
|
||||
};
|
||||
|
||||
use super::{
|
||||
config::NetworkBrokerConfigErr,
|
||||
msgs::{Envelope, SetPskResponse},
|
||||
api::msgs::{self, EnvelopeExt, SetPskRequestExt, SetPskResponseExt},
|
||||
WireGuardBroker,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
|
||||
@@ -28,20 +22,16 @@ impl<RecvError> From<msgs::InvalidMessageTypeError> for BrokerClientPollResponse
|
||||
}
|
||||
}
|
||||
|
||||
fn io_poller<RecvError>(e: RecvError) -> BrokerClientPollResponseError<RecvError> {
|
||||
fn io_pollerr<RecvError>(e: RecvError) -> BrokerClientPollResponseError<RecvError> {
|
||||
BrokerClientPollResponseError::<RecvError>::IoError(e)
|
||||
}
|
||||
|
||||
fn invalid_msg_poller<RecvError>() -> BrokerClientPollResponseError<RecvError> {
|
||||
fn invalid_msg_pollerr<RecvError>() -> BrokerClientPollResponseError<RecvError> {
|
||||
BrokerClientPollResponseError::<RecvError>::InvalidMessage
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
|
||||
pub enum BrokerClientSetPskError<SendError> {
|
||||
#[error("Error with encoding/decoding message")]
|
||||
MsgError,
|
||||
#[error("Network Broker Config error: {0}")]
|
||||
BrokerError(NetworkBrokerConfigErr),
|
||||
#[error(transparent)]
|
||||
IoError(SendError),
|
||||
#[error("Interface name out of bounds")]
|
||||
@@ -57,91 +47,104 @@ pub trait BrokerClientIo {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BrokerClient<Io>
|
||||
pub struct BrokerClient<'a, Io, IoRef>
|
||||
where
|
||||
Io: BrokerClientIo + Debug,
|
||||
Io: BrokerClientIo,
|
||||
IoRef: 'a + BorrowMut<Io>,
|
||||
{
|
||||
io: Io,
|
||||
io: IoRef,
|
||||
_phantom_io: PhantomData<&'a mut Io>,
|
||||
}
|
||||
|
||||
impl<Io> BrokerClient<Io>
|
||||
impl<'a, Io, IoRef> BrokerClient<'a, Io, IoRef>
|
||||
where
|
||||
Io: BrokerClientIo + Debug,
|
||||
Io: BrokerClientIo,
|
||||
IoRef: 'a + BorrowMut<Io>,
|
||||
{
|
||||
pub fn new(io: Io) -> Self {
|
||||
Self { io }
|
||||
pub fn new(io: IoRef) -> Self {
|
||||
Self {
|
||||
io,
|
||||
_phantom_io: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn io(&self) -> &Io {
|
||||
pub fn io(&self) -> &IoRef {
|
||||
&self.io
|
||||
}
|
||||
|
||||
pub fn io_mut(&mut self) -> &mut Io {
|
||||
pub fn io_mut(&mut self) -> &mut IoRef {
|
||||
&mut self.io
|
||||
}
|
||||
|
||||
pub fn poll_response(
|
||||
&mut self,
|
||||
) -> Result<Option<msgs::SetPskResult>, BrokerClientPollResponseError<Io::RecvError>> {
|
||||
let res: &[u8] = match self.io.borrow_mut().recv_msg().map_err(io_poller)? {
|
||||
let res: &[u8] = match self.io.borrow_mut().recv_msg().map_err(io_pollerr)? {
|
||||
Some(r) => r,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let typ = res.get(0).ok_or(invalid_msg_poller())?;
|
||||
let typ = res.get(0).ok_or(invalid_msg_pollerr())?;
|
||||
let typ = msgs::MsgType::try_from(*typ)?;
|
||||
let msgs::MsgType::SetPsk = typ; // Assert type
|
||||
|
||||
let res = zerocopy::Ref::<&[u8], Envelope<SetPskResponse>>::new(res)
|
||||
.ok_or(invalid_msg_poller())?;
|
||||
let res: &msgs::SetPskResponse = &res.payload;
|
||||
let res: msgs::SetPskResponseReturnCode = res
|
||||
.return_code
|
||||
let res: msgs::Envelope<_, msgs::SetPskResponse<&[u8]>> = res
|
||||
.envelope_truncating()
|
||||
.map_err(|_| invalid_msg_pollerr())?;
|
||||
let res: msgs::SetPskResponse<&[u8]> = res
|
||||
.payload()
|
||||
.set_psk_response()
|
||||
.map_err(|_| invalid_msg_pollerr())?;
|
||||
let res: msgs::SetPskResponseReturnCode = res.return_code()[0]
|
||||
.try_into()
|
||||
.map_err(|_| invalid_msg_poller())?;
|
||||
.map_err(|_| invalid_msg_pollerr())?;
|
||||
let res: msgs::SetPskResult = res.into();
|
||||
|
||||
Ok(Some(res))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io> WireGuardBroker for BrokerClient<Io>
|
||||
impl<'a, Io, IoRef> WireGuardBroker for BrokerClient<'a, Io, IoRef>
|
||||
where
|
||||
Io: BrokerClientIo + Debug,
|
||||
Io: BrokerClientIo,
|
||||
IoRef: 'a + BorrowMut<Io>,
|
||||
{
|
||||
type Error = BrokerClientSetPskError<Io::SendError>;
|
||||
|
||||
fn set_psk(&mut self, config: SerializedBrokerConfig) -> Result<(), Self::Error> {
|
||||
let config: Result<NetworkBrokerConfig, NetworkBrokerConfigErr> = config.try_into();
|
||||
let config = config.map_err(|e| BrokerClientSetPskError::BrokerError(e))?;
|
||||
|
||||
fn set_psk(
|
||||
&mut self,
|
||||
iface: &str,
|
||||
peer_id: [u8; 32],
|
||||
psk: [u8; 32],
|
||||
) -> Result<(), Self::Error> {
|
||||
use BrokerClientSetPskError::*;
|
||||
const BUF_SIZE: usize = REQUEST_MSG_BUFFER_SIZE;
|
||||
const BUF_SIZE: usize = <msgs::Envelope<(), msgs::SetPskRequest<()>> as LenseView>::LEN;
|
||||
|
||||
// Allocate message
|
||||
let mut req = [0u8; BUF_SIZE];
|
||||
|
||||
// Construct message view
|
||||
let mut req = zerocopy::Ref::<&mut [u8], Envelope<msgs::SetPskRequest>>::new(&mut req)
|
||||
.ok_or(MsgError)?;
|
||||
let mut req: msgs::Envelope<_, msgs::SetPskRequest<&mut [u8]>> =
|
||||
(&mut req as &mut [u8]).envelope_truncating().unwrap();
|
||||
|
||||
// Populate envelope
|
||||
req.msg_type = msgs::MsgType::SetPsk as u8;
|
||||
req.msg_type_mut()
|
||||
.copy_from_slice(&[msgs::MsgType::SetPsk as u8]);
|
||||
{
|
||||
// Derived payload
|
||||
let req = &mut req.payload;
|
||||
let mut req: msgs::SetPskRequest<&mut [u8]> =
|
||||
req.payload_mut().set_psk_request().unwrap();
|
||||
|
||||
// Populate payload
|
||||
req.peer_id.copy_from_slice(&config.peer_id.value);
|
||||
req.psk.copy_from_slice(config.psk.secret());
|
||||
req.set_iface(config.iface.as_ref())
|
||||
.ok_or(IfaceOutOfBounds)?;
|
||||
req.peer_id_mut().copy_from_slice(&peer_id);
|
||||
req.psk_mut().copy_from_slice(&psk);
|
||||
req.set_iface(iface).ok_or(IfaceOutOfBounds)?;
|
||||
}
|
||||
|
||||
// Send message
|
||||
self.io
|
||||
.borrow_mut()
|
||||
.send_msg(req.bytes())
|
||||
.send_msg(req.all_bytes())
|
||||
.map_err(IoError)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
use crate::{SerializedBrokerConfig, WG_KEY_LEN, WG_PEER_LEN};
|
||||
use derive_builder::Builder;
|
||||
use rosenpass_secret_memory::{Public, Secret};
|
||||
|
||||
#[derive(Builder)]
|
||||
#[builder(pattern = "mutable")]
|
||||
//TODO: Use generics for iface, add additional params
|
||||
pub struct NetworkBrokerConfig<'a> {
|
||||
pub iface: &'a str,
|
||||
pub peer_id: &'a Public<WG_PEER_LEN>,
|
||||
pub psk: &'a Secret<WG_KEY_LEN>,
|
||||
}
|
||||
|
||||
impl<'a> Into<SerializedBrokerConfig<'a>> for NetworkBrokerConfig<'a> {
|
||||
fn into(self) -> SerializedBrokerConfig<'a> {
|
||||
SerializedBrokerConfig {
|
||||
interface: self.iface.as_bytes(),
|
||||
peer_id: self.peer_id,
|
||||
psk: self.psk,
|
||||
additional_params: &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
|
||||
pub enum NetworkBrokerConfigErr {
|
||||
#[error("Interface")]
|
||||
Interface,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<SerializedBrokerConfig<'a>> for NetworkBrokerConfig<'a> {
|
||||
type Error = NetworkBrokerConfigErr;
|
||||
|
||||
fn try_from(value: SerializedBrokerConfig<'a>) -> Result<Self, Self::Error> {
|
||||
let iface =
|
||||
std::str::from_utf8(value.interface).map_err(|_| NetworkBrokerConfigErr::Interface)?;
|
||||
Ok(Self {
|
||||
iface,
|
||||
peer_id: value.peer_id,
|
||||
psk: value.psk,
|
||||
})
|
||||
}
|
||||
}
|
||||
204
wireguard-broker/src/api/mio_client.rs
Normal file
204
wireguard-broker/src/api/mio_client.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
|
||||
use crate::WireGuardBroker;
|
||||
|
||||
use super::client::{
|
||||
BrokerClient, BrokerClientIo, BrokerClientPollResponseError, BrokerClientSetPskError,
|
||||
};
|
||||
use super::msgs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MioBrokerClient {
|
||||
inner: BrokerClient<'static, MioBrokerClientIo, MioBrokerClientIo>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MioBrokerClientIo {
|
||||
socket: mio::net::UnixStream,
|
||||
send_buf: VecDeque<u8>,
|
||||
receiving_size: bool,
|
||||
recv_buf: Vec<u8>,
|
||||
recv_off: usize,
|
||||
}
|
||||
|
||||
impl MioBrokerClient {
|
||||
pub fn new(socket: mio::net::UnixStream) -> Self {
|
||||
let io = MioBrokerClientIo {
|
||||
socket,
|
||||
send_buf: VecDeque::new(),
|
||||
receiving_size: false,
|
||||
recv_buf: Vec::new(),
|
||||
recv_off: 0,
|
||||
};
|
||||
let inner = BrokerClient::new(io);
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn poll(&mut self) -> anyhow::Result<Option<msgs::SetPskResult>> {
|
||||
self.inner.io_mut().flush()?;
|
||||
|
||||
// This sucks
|
||||
match self.inner.poll_response() {
|
||||
Ok(res) => {
|
||||
return Ok(res);
|
||||
}
|
||||
Err(BrokerClientPollResponseError::IoError(e)) => {
|
||||
return Err(e);
|
||||
}
|
||||
Err(BrokerClientPollResponseError::InvalidMessage) => {
|
||||
bail!("Invalid message");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl WireGuardBroker for MioBrokerClient {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn set_psk(&mut self, iface: &str, peer_id: [u8; 32], psk: [u8; 32]) -> anyhow::Result<()> {
|
||||
use BrokerClientSetPskError::*;
|
||||
let e = self.inner.set_psk(iface, peer_id, psk);
|
||||
match e {
|
||||
Ok(()) => Ok(()),
|
||||
Err(IoError(e)) => Err(e),
|
||||
Err(IfaceOutOfBounds) => bail!("Interface name size is out of bounds."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BrokerClientIo for MioBrokerClientIo {
|
||||
type SendError = anyhow::Error;
|
||||
type RecvError = anyhow::Error;
|
||||
|
||||
fn send_msg(&mut self, buf: &[u8]) -> Result<(), Self::SendError> {
|
||||
self.flush()?;
|
||||
self.send_or_buffer(&(buf.len() as u64).to_le_bytes())?;
|
||||
self.send_or_buffer(&buf)?;
|
||||
self.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv_msg(&mut self) -> Result<Option<&[u8]>, Self::RecvError> {
|
||||
// Stale message in receive buffer. Reset!
|
||||
if self.recv_off == self.recv_buf.len() {
|
||||
self.receiving_size = true;
|
||||
self.recv_off = 0;
|
||||
self.recv_buf.resize(8, 0);
|
||||
}
|
||||
|
||||
// Try filling the receive buffer
|
||||
self.recv_off += raw_recv(&self.socket, &mut self.recv_buf[self.recv_off..])?;
|
||||
if self.recv_off < self.recv_buf.len() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Received size, now start receiving
|
||||
if self.receiving_size {
|
||||
// Received the size
|
||||
// Parse the received length
|
||||
let len: &[u8; 8] = self.recv_buf[..].try_into().unwrap();
|
||||
let len: usize = u64::from_le_bytes(*len) as usize;
|
||||
|
||||
ensure!(
|
||||
len <= msgs::RESPONSE_MSG_BUFFER_SIZE,
|
||||
"Oversized buffer ({len}) in psk buffer response."
|
||||
);
|
||||
|
||||
// Prepare the message buffer for receiving an actual message of the given size
|
||||
self.receiving_size = false;
|
||||
self.recv_off = 0;
|
||||
self.recv_buf.resize(len, 0);
|
||||
|
||||
// Try to receive the message
|
||||
return self.recv_msg();
|
||||
}
|
||||
|
||||
// Received an actual message
|
||||
return Ok(Some(&self.recv_buf[..]));
|
||||
}
|
||||
}
|
||||
|
||||
impl MioBrokerClientIo {
|
||||
fn flush(&mut self) -> anyhow::Result<()> {
|
||||
let (fst, snd) = self.send_buf.as_slices();
|
||||
|
||||
let (written, res) = match raw_send(&self.socket, fst) {
|
||||
Ok(w1) if w1 >= fst.len() => match raw_send(&self.socket, snd) {
|
||||
Ok(w2) => (w1 + w2, Ok(())),
|
||||
Err(e) => (w1, Err(e)),
|
||||
},
|
||||
Ok(w1) => (w1, Ok(())),
|
||||
Err(e) => (0, Err(e)),
|
||||
};
|
||||
|
||||
self.send_buf.drain(..written);
|
||||
|
||||
(&self.socket).try_io(|| (&self.socket).flush())?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn send_or_buffer(&mut self, buf: &[u8]) -> anyhow::Result<()> {
|
||||
let mut off = 0;
|
||||
|
||||
if self.send_buf.is_empty() {
|
||||
off += raw_send(&self.socket, buf)?;
|
||||
}
|
||||
|
||||
self.send_buf.extend((&buf[off..]).iter());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_send(mut socket: &mio::net::UnixStream, data: &[u8]) -> anyhow::Result<usize> {
|
||||
let mut off = 0;
|
||||
|
||||
socket.try_io(|| {
|
||||
loop {
|
||||
if off == data.len() {
|
||||
return Ok(());
|
||||
}
|
||||
match socket.write(&data[off..]) {
|
||||
Ok(n) => {
|
||||
off += n;
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
||||
// pass – retry
|
||||
}
|
||||
Err(e) if off > 0 || e.kind() == ErrorKind::WouldBlock => return Ok(()),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
return Ok(off);
|
||||
}
|
||||
|
||||
fn raw_recv(mut socket: &mio::net::UnixStream, out: &mut [u8]) -> anyhow::Result<usize> {
|
||||
let mut off = 0;
|
||||
|
||||
socket.try_io(|| {
|
||||
loop {
|
||||
if off == out.len() {
|
||||
return Ok(());
|
||||
}
|
||||
match socket.read(&mut out[off..]) {
|
||||
Ok(n) => {
|
||||
off += n;
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
||||
// pass – retry
|
||||
}
|
||||
Err(e) if off > 0 || e.kind() == ErrorKind::WouldBlock => return Ok(()),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
return Ok(off);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod mio_client;
|
||||
pub mod msgs;
|
||||
pub mod server;
|
||||
|
||||
@@ -1,49 +1,46 @@
|
||||
use std::result::Result;
|
||||
use std::str::{from_utf8, Utf8Error};
|
||||
|
||||
use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
use rosenpass_lenses::{lense, LenseView};
|
||||
|
||||
pub const ENVELOPE_OVERHEAD: usize = 1 + 3;
|
||||
pub const REQUEST_MSG_BUFFER_SIZE: usize = ENVELOPE_OVERHEAD + 32 + 32 + 1 + 255;
|
||||
pub const RESPONSE_MSG_BUFFER_SIZE: usize = ENVELOPE_OVERHEAD + 1;
|
||||
pub const REQUEST_MSG_BUFFER_SIZE: usize = <Envelope<(), SetPskRequest<()>> as LenseView>::LEN;
|
||||
pub const RESPONSE_MSG_BUFFER_SIZE: usize = <Envelope<(), SetPskResponse<()>> as LenseView>::LEN;
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct Envelope<M: AsBytes + FromBytes> {
|
||||
lense! { Envelope<M> :=
|
||||
/// [MsgType] of this message
|
||||
pub msg_type: u8,
|
||||
msg_type: 1,
|
||||
/// Reserved for future use
|
||||
pub reserved: [u8; 3],
|
||||
reserved: 3,
|
||||
/// The actual Paylod
|
||||
pub payload: M,
|
||||
payload: M::LEN
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct SetPskRequest {
|
||||
pub peer_id: [u8; 32],
|
||||
pub psk: [u8; 32],
|
||||
pub iface_size: u8, // TODO: We should have variable length strings in lenses
|
||||
pub iface_buf: [u8; 255],
|
||||
lense! { SetPskRequest :=
|
||||
peer_id: 32,
|
||||
psk: 32,
|
||||
iface_size: 1, // TODO: We should have variable length strings in lenses
|
||||
iface_buf: 255
|
||||
}
|
||||
|
||||
impl SetPskRequest {
|
||||
impl SetPskRequest<&[u8]> {
|
||||
pub fn iface_bin(&self) -> &[u8] {
|
||||
let len = self.iface_size as usize;
|
||||
&self.iface_buf[..len]
|
||||
let len = self.iface_size()[0] as usize;
|
||||
&self.iface_buf()[..len]
|
||||
}
|
||||
|
||||
pub fn iface(&self) -> Result<&str, Utf8Error> {
|
||||
from_utf8(self.iface_bin())
|
||||
}
|
||||
}
|
||||
|
||||
impl SetPskRequest<&mut [u8]> {
|
||||
pub fn set_iface_bin(&mut self, iface: &[u8]) -> Option<()> {
|
||||
(iface.len() < 256).then_some(())?; // Assert iface.len() < 256
|
||||
|
||||
self.iface_size = iface.len() as u8;
|
||||
self.iface_size_mut()[0] = iface.len() as u8;
|
||||
|
||||
self.iface_buf = [0; 255];
|
||||
(&mut self.iface_buf[..iface.len()]).copy_from_slice(iface);
|
||||
self.iface_buf_mut().fill(0);
|
||||
(&mut self.iface_buf_mut()[..iface.len()]).copy_from_slice(iface);
|
||||
|
||||
Some(())
|
||||
}
|
||||
@@ -53,10 +50,8 @@ impl SetPskRequest {
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct SetPskResponse {
|
||||
pub return_code: u8,
|
||||
lense! { SetPskResponse :=
|
||||
return_code: 1
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
use std::borrow::BorrowMut;
|
||||
use std::marker::PhantomData;
|
||||
use std::result::Result;
|
||||
|
||||
use rosenpass_secret_memory::{Public, Secret};
|
||||
use rosenpass_lenses::LenseError;
|
||||
|
||||
use crate::api::msgs::{self, Envelope, SetPskRequest, SetPskResponse};
|
||||
use crate::api::msgs::{self, EnvelopeExt, SetPskRequestExt, SetPskResponseExt};
|
||||
use crate::WireGuardBroker;
|
||||
|
||||
use super::config::{NetworkBrokerConfigBuilder, NetworkBrokerConfigErr};
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
|
||||
pub enum BrokerServerError {
|
||||
#[error("No such request type: {}", .0)]
|
||||
NoSuchRequestType(u8),
|
||||
#[error("Invalid message received.")]
|
||||
InvalidMessage,
|
||||
#[error("Network Broker Config error: {0}")]
|
||||
BrokerError(NetworkBrokerConfigErr),
|
||||
}
|
||||
|
||||
impl From<LenseError> for BrokerServerError {
|
||||
fn from(value: LenseError) -> Self {
|
||||
use BrokerServerError as Be;
|
||||
use LenseError as Le;
|
||||
match value {
|
||||
Le::BufferSizeMismatch => Be::InvalidMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<msgs::InvalidMessageTypeError> for BrokerServerError {
|
||||
@@ -25,21 +32,27 @@ impl From<msgs::InvalidMessageTypeError> for BrokerServerError {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BrokerServer<Err, Inner>
|
||||
pub struct BrokerServer<'a, Err, Inner, Ref>
|
||||
where
|
||||
Inner: WireGuardBroker<Error = Err>,
|
||||
msgs::SetPskError: From<Err>,
|
||||
Inner: WireGuardBroker<Error = Err>,
|
||||
Ref: BorrowMut<Inner> + 'a,
|
||||
{
|
||||
inner: Inner,
|
||||
inner: Ref,
|
||||
_phantom: PhantomData<&'a mut Inner>,
|
||||
}
|
||||
|
||||
impl<Err, Inner> BrokerServer<Err, Inner>
|
||||
impl<'a, Err, Inner, Ref> BrokerServer<'a, Err, Inner, Ref>
|
||||
where
|
||||
Inner: WireGuardBroker<Error = Err>,
|
||||
msgs::SetPskError: From<Err>,
|
||||
Inner: WireGuardBroker<Error = Err>,
|
||||
Ref: 'a + BorrowMut<Inner>,
|
||||
{
|
||||
pub fn new(inner: Inner) -> Self {
|
||||
Self { inner }
|
||||
pub fn new(inner: Ref) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_message(
|
||||
@@ -53,40 +66,33 @@ where
|
||||
let typ = msgs::MsgType::try_from(*typ)?;
|
||||
let msgs::MsgType::SetPsk = typ; // Assert type
|
||||
|
||||
let req = zerocopy::Ref::<&[u8], Envelope<SetPskRequest>>::new(req)
|
||||
.ok_or(BrokerServerError::InvalidMessage)?;
|
||||
let mut res = zerocopy::Ref::<&mut [u8], Envelope<SetPskResponse>>::new(res)
|
||||
.ok_or(BrokerServerError::InvalidMessage)?;
|
||||
|
||||
res.payload.return_code = msgs::MsgType::SetPsk as u8;
|
||||
self.handle_set_psk(&req.payload, &mut res.payload)?;
|
||||
Ok(res.bytes().len())
|
||||
let req: msgs::Envelope<_, msgs::SetPskRequest<&[u8]>> = req.envelope_truncating()?;
|
||||
let mut res: msgs::Envelope<_, msgs::SetPskResponse<&mut [u8]>> =
|
||||
(res as &mut [u8]).envelope_truncating()?;
|
||||
(&mut res).msg_type_mut()[0] = msgs::MsgType::SetPsk as u8;
|
||||
self.handle_set_psk(
|
||||
req.payload().set_psk_request()?,
|
||||
res.payload_mut().set_psk_response()?,
|
||||
)?;
|
||||
Ok(res.all_bytes().len())
|
||||
}
|
||||
|
||||
fn handle_set_psk(
|
||||
&mut self,
|
||||
req: &SetPskRequest,
|
||||
res: &mut SetPskResponse,
|
||||
req: msgs::SetPskRequest<&[u8]>,
|
||||
mut res: msgs::SetPskResponse<&mut [u8]>,
|
||||
) -> Result<(), BrokerServerError> {
|
||||
// Using unwrap here since lenses can not return fixed-size arrays
|
||||
// TODO: Slices should give access to fixed size arrays
|
||||
let peer_id = Public::from_slice(&req.peer_id);
|
||||
let psk = Secret::from_slice(&req.psk);
|
||||
|
||||
let interface = req
|
||||
.iface()
|
||||
.map_err(|_e| BrokerServerError::InvalidMessage)?;
|
||||
|
||||
let config = NetworkBrokerConfigBuilder::default()
|
||||
.peer_id(&peer_id)
|
||||
.psk(&psk)
|
||||
.iface(interface)
|
||||
.build()
|
||||
.unwrap();
|
||||
let r: Result<(), Err> = self.inner.borrow_mut().set_psk(config.into());
|
||||
let r: Result<(), Err> = self.inner.borrow_mut().set_psk(
|
||||
req.iface()
|
||||
.map_err(|_e| BrokerServerError::InvalidMessage)?,
|
||||
req.peer_id().try_into().unwrap(),
|
||||
req.psk().try_into().unwrap(),
|
||||
);
|
||||
let r: msgs::SetPskResult = r.map_err(|e| e.into());
|
||||
let r: msgs::SetPskResponseReturnCode = r.into();
|
||||
res.return_code = r as u8;
|
||||
res.return_code_mut()[0] = r as u8;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,67 +1,56 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::main().unwrap();
|
||||
use std::io::{stdin, stdout, Read, Write};
|
||||
use std::result::Result;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
panic!("This binary is only supported on Linux");
|
||||
use rosenpass_wireguard_broker::api::msgs;
|
||||
use rosenpass_wireguard_broker::api::server::BrokerServer;
|
||||
use rosenpass_wireguard_broker::netlink as wg;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum BrokerAppError {
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
WgConnectError(#[from] wg::ConnectError),
|
||||
#[error(transparent)]
|
||||
WgSetPskError(#[from] wg::SetPskError),
|
||||
#[error("Oversized message {}; something about the request is fatally wrong", .0)]
|
||||
OversizedMessage(u64),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux {
|
||||
use std::io::{stdin, stdout, Read, Write};
|
||||
use std::result::Result;
|
||||
fn main() -> Result<(), BrokerAppError> {
|
||||
let mut broker = BrokerServer::new(wg::NetlinkWireGuardBroker::new()?);
|
||||
|
||||
use rosenpass_wireguard_broker::api::msgs;
|
||||
use rosenpass_wireguard_broker::api::server::BrokerServer;
|
||||
use rosenpass_wireguard_broker::brokers::netlink as wg;
|
||||
let mut stdin = stdin().lock();
|
||||
let mut stdout = stdout().lock();
|
||||
loop {
|
||||
// Read the message length
|
||||
let mut len = [0u8; 8];
|
||||
stdin.read_exact(&mut len)?;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum BrokerAppError {
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
WgConnectError(#[from] wg::ConnectError),
|
||||
#[error(transparent)]
|
||||
WgSetPskError(#[from] wg::SetPskError),
|
||||
#[error("Oversized message {}; something about the request is fatally wrong", .0)]
|
||||
OversizedMessage(u64),
|
||||
}
|
||||
|
||||
pub fn main() -> Result<(), BrokerAppError> {
|
||||
let mut broker = BrokerServer::new(wg::NetlinkWireGuardBroker::new()?);
|
||||
|
||||
let mut stdin = stdin().lock();
|
||||
let mut stdout = stdout().lock();
|
||||
loop {
|
||||
// Read the message length
|
||||
let mut len = [0u8; 8];
|
||||
stdin.read_exact(&mut len)?;
|
||||
|
||||
// Parse the message length
|
||||
let len = u64::from_le_bytes(len);
|
||||
if (len as usize) > msgs::REQUEST_MSG_BUFFER_SIZE {
|
||||
return Err(BrokerAppError::OversizedMessage(len));
|
||||
}
|
||||
|
||||
// Read the message itself
|
||||
let mut req_buf = [0u8; msgs::REQUEST_MSG_BUFFER_SIZE];
|
||||
let req_buf = &mut req_buf[..(len as usize)];
|
||||
stdin.read_exact(req_buf)?;
|
||||
|
||||
// Process the message
|
||||
let mut res_buf = [0u8; msgs::RESPONSE_MSG_BUFFER_SIZE];
|
||||
let res = match broker.handle_message(req_buf, &mut res_buf) {
|
||||
Ok(len) => &res_buf[..len],
|
||||
Err(e) => {
|
||||
eprintln!("Error processing message for wireguard PSK broker: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Write the response
|
||||
stdout.write_all(&(res.len() as u64).to_le_bytes())?;
|
||||
stdout.write_all(&res)?;
|
||||
stdout.flush()?;
|
||||
// Parse the message length
|
||||
let len = u64::from_le_bytes(len);
|
||||
if (len as usize) > msgs::REQUEST_MSG_BUFFER_SIZE {
|
||||
return Err(BrokerAppError::OversizedMessage(len));
|
||||
}
|
||||
|
||||
// Read the message itself
|
||||
let mut req_buf = [0u8; msgs::REQUEST_MSG_BUFFER_SIZE];
|
||||
let req_buf = &mut req_buf[..(len as usize)];
|
||||
stdin.read_exact(req_buf)?;
|
||||
|
||||
// Process the message
|
||||
let mut res_buf = [0u8; msgs::RESPONSE_MSG_BUFFER_SIZE];
|
||||
let res = match broker.handle_message(req_buf, &mut res_buf) {
|
||||
Ok(len) => &res_buf[..len],
|
||||
Err(e) => {
|
||||
eprintln!("Error processing message for wireguard PSK broker: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Write the response
|
||||
stdout.write_all(&(res.len() as u64).to_le_bytes())?;
|
||||
stdout.write_all(&res)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
use anyhow::{bail, ensure};
|
||||
use mio::Interest;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
|
||||
use crate::{SerializedBrokerConfig, WireGuardBroker, WireguardBrokerMio};
|
||||
|
||||
use crate::api::client::{
|
||||
BrokerClient, BrokerClientIo, BrokerClientPollResponseError, BrokerClientSetPskError,
|
||||
};
|
||||
use crate::api::msgs::{self, RESPONSE_MSG_BUFFER_SIZE};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MioBrokerClient {
|
||||
inner: BrokerClient<MioBrokerClientIo>,
|
||||
}
|
||||
|
||||
const LEN_SIZE: usize = 8;
|
||||
const RECV_BUF_SIZE: usize = RESPONSE_MSG_BUFFER_SIZE;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MioBrokerClientIo {
|
||||
socket: mio::net::UnixStream,
|
||||
send_buf: VecDeque<u8>,
|
||||
recv_state: RxState,
|
||||
expected_state: RxState,
|
||||
recv_buf: [u8; RECV_BUF_SIZE],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RxState {
|
||||
//Recieving size with buffer offset
|
||||
RxSize(usize),
|
||||
RxBuffer(usize),
|
||||
}
|
||||
|
||||
impl MioBrokerClient {
|
||||
pub fn new(socket: mio::net::UnixStream) -> Self {
|
||||
let io = MioBrokerClientIo {
|
||||
socket,
|
||||
send_buf: VecDeque::new(),
|
||||
recv_state: RxState::RxSize(0),
|
||||
recv_buf: [0u8; RECV_BUF_SIZE],
|
||||
expected_state: RxState::RxSize(LEN_SIZE),
|
||||
};
|
||||
let inner = BrokerClient::new(io);
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
fn poll(&mut self) -> anyhow::Result<Option<msgs::SetPskResult>> {
|
||||
self.inner.io_mut().flush()?;
|
||||
|
||||
// This sucks
|
||||
match self.inner.poll_response() {
|
||||
Ok(res) => {
|
||||
return Ok(res);
|
||||
}
|
||||
Err(BrokerClientPollResponseError::IoError(e)) => {
|
||||
return Err(e);
|
||||
}
|
||||
Err(BrokerClientPollResponseError::InvalidMessage) => {
|
||||
bail!("Invalid message");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl WireGuardBroker for MioBrokerClient {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn set_psk<'a>(&mut self, config: SerializedBrokerConfig<'a>) -> anyhow::Result<()> {
|
||||
use BrokerClientSetPskError::*;
|
||||
let e = self.inner.set_psk(config);
|
||||
match e {
|
||||
Ok(()) => Ok(()),
|
||||
Err(IoError(e)) => Err(e),
|
||||
Err(IfaceOutOfBounds) => bail!("Interface name size is out of bounds."),
|
||||
Err(MsgError) => bail!("Error with encoding/decoding message."),
|
||||
Err(BrokerError(e)) => bail!("Broker error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WireguardBrokerMio for MioBrokerClient {
|
||||
type MioError = anyhow::Error;
|
||||
|
||||
fn register(
|
||||
&mut self,
|
||||
registry: &mio::Registry,
|
||||
token: mio::Token,
|
||||
) -> Result<(), Self::MioError> {
|
||||
registry.register(
|
||||
&mut self.inner.io_mut().socket,
|
||||
token,
|
||||
Interest::READABLE | Interest::WRITABLE,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_poll(&mut self) -> Result<(), Self::MioError> {
|
||||
self.poll()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister(&mut self, registry: &mio::Registry) -> Result<(), Self::MioError> {
|
||||
registry.deregister(&mut self.inner.io_mut().socket)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BrokerClientIo for MioBrokerClientIo {
|
||||
type SendError = anyhow::Error;
|
||||
type RecvError = anyhow::Error;
|
||||
|
||||
fn send_msg(&mut self, buf: &[u8]) -> Result<(), Self::SendError> {
|
||||
self.flush()?;
|
||||
self.send_or_buffer(&(buf.len() as u64).to_le_bytes())?;
|
||||
self.send_or_buffer(&buf)?;
|
||||
self.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv_msg(&mut self) -> Result<Option<&[u8]>, Self::RecvError> {
|
||||
loop {
|
||||
match (self.recv_state, self.expected_state) {
|
||||
//Stale Buffer state or recieved everything
|
||||
(RxState::RxSize(x), RxState::RxSize(y))
|
||||
| (RxState::RxBuffer(x), RxState::RxBuffer(y))
|
||||
if x == y =>
|
||||
{
|
||||
match self.recv_state {
|
||||
RxState::RxSize(s) => {
|
||||
let len: &[u8; LEN_SIZE] = self.recv_buf[0..s].try_into().unwrap();
|
||||
let len: usize = u64::from_le_bytes(*len) as usize;
|
||||
|
||||
ensure!(
|
||||
len <= msgs::RESPONSE_MSG_BUFFER_SIZE,
|
||||
"Oversized buffer ({len}) in psk buffer response."
|
||||
);
|
||||
|
||||
self.recv_state = RxState::RxBuffer(0);
|
||||
self.expected_state = RxState::RxBuffer(len);
|
||||
continue;
|
||||
}
|
||||
RxState::RxBuffer(s) => {
|
||||
self.recv_state = RxState::RxSize(0);
|
||||
self.expected_state = RxState::RxSize(LEN_SIZE);
|
||||
return Ok(Some(&self.recv_buf[0..s]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Recieve if x < y
|
||||
(RxState::RxSize(x), RxState::RxSize(y))
|
||||
| (RxState::RxBuffer(x), RxState::RxBuffer(y))
|
||||
if x < y =>
|
||||
{
|
||||
let bytes = raw_recv(&self.socket, &mut self.recv_buf[x..y])?;
|
||||
|
||||
if x + bytes == y {
|
||||
return Ok(Some(&self.recv_buf[0..y]));
|
||||
}
|
||||
//We didn't recieve everything so let's assume something went wrong
|
||||
self.recv_state = RxState::RxSize(0);
|
||||
self.expected_state = RxState::RxSize(LEN_SIZE);
|
||||
bail!("Invalid state");
|
||||
}
|
||||
_ => {
|
||||
//Reset states
|
||||
self.recv_state = RxState::RxSize(0);
|
||||
self.expected_state = RxState::RxSize(LEN_SIZE);
|
||||
bail!("Invalid state");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MioBrokerClientIo {
|
||||
fn flush(&mut self) -> anyhow::Result<()> {
|
||||
let (fst, snd) = self.send_buf.as_slices();
|
||||
|
||||
let (written, res) = match raw_send(&self.socket, fst) {
|
||||
Ok(w1) if w1 >= fst.len() => match raw_send(&self.socket, snd) {
|
||||
Ok(w2) => (w1 + w2, Ok(())),
|
||||
Err(e) => (w1, Err(e)),
|
||||
},
|
||||
Ok(w1) => (w1, Ok(())),
|
||||
Err(e) => (0, Err(e)),
|
||||
};
|
||||
|
||||
self.send_buf.drain(..written);
|
||||
|
||||
(&self.socket).try_io(|| (&self.socket).flush())?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn send_or_buffer(&mut self, buf: &[u8]) -> anyhow::Result<()> {
|
||||
let mut off = 0;
|
||||
|
||||
if self.send_buf.is_empty() {
|
||||
off += raw_send(&self.socket, buf)?;
|
||||
}
|
||||
|
||||
self.send_buf.extend((&buf[off..]).iter());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_send(mut socket: &mio::net::UnixStream, data: &[u8]) -> anyhow::Result<usize> {
|
||||
let mut off = 0;
|
||||
|
||||
socket.try_io(|| {
|
||||
loop {
|
||||
if off == data.len() {
|
||||
return Ok(());
|
||||
}
|
||||
match socket.write(&data[off..]) {
|
||||
Ok(n) => {
|
||||
off += n;
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
||||
// pass – retry
|
||||
}
|
||||
Err(e) if off > 0 || e.kind() == ErrorKind::WouldBlock => return Ok(()),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
return Ok(off);
|
||||
}
|
||||
|
||||
fn raw_recv(mut socket: &mio::net::UnixStream, out: &mut [u8]) -> anyhow::Result<usize> {
|
||||
let mut off = 0;
|
||||
|
||||
socket.try_io(|| {
|
||||
loop {
|
||||
if off == out.len() {
|
||||
return Ok(());
|
||||
}
|
||||
match socket.read(&mut out[off..]) {
|
||||
Ok(n) => {
|
||||
off += n;
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
||||
// pass – retry
|
||||
}
|
||||
Err(e) if off > 0 || e.kind() == ErrorKind::WouldBlock => return Ok(()),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
return Ok(off);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#[cfg(feature = "enable_broker_api")]
|
||||
pub mod mio_client;
|
||||
#[cfg(all(feature = "enable_broker_api", target_os = "linux"))]
|
||||
pub mod netlink;
|
||||
|
||||
pub mod native_unix;
|
||||
@@ -1,177 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use log::{debug, error};
|
||||
use postcard::{from_bytes, to_allocvec};
|
||||
use rosenpass_secret_memory::{Public, Secret};
|
||||
use rosenpass_util::b64::b64_decode;
|
||||
use rosenpass_util::{b64::B64Display, file::StoreValueB64Writer};
|
||||
|
||||
use crate::{SerializedBrokerConfig, WireGuardBroker, WireguardBrokerCfg, WireguardBrokerMio};
|
||||
use crate::{WG_KEY_LEN, WG_PEER_LEN};
|
||||
|
||||
const MAX_B64_KEY_SIZE: usize = WG_KEY_LEN * 5 / 3;
|
||||
const MAX_B64_PEER_ID_SIZE: usize = WG_PEER_LEN * 5 / 3;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NativeUnixBroker {}
|
||||
|
||||
impl Default for NativeUnixBroker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl NativeUnixBroker {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl WireGuardBroker for NativeUnixBroker {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn set_psk(&mut self, config: SerializedBrokerConfig<'_>) -> Result<(), Self::Error> {
|
||||
let config: NativeUnixBrokerConfig = config.try_into()?;
|
||||
|
||||
let peer_id = format!("{}", config.peer_id.fmt_b64::<MAX_B64_PEER_ID_SIZE>());
|
||||
|
||||
let mut child = match Command::new("wg")
|
||||
.arg("set")
|
||||
.arg(config.interface)
|
||||
.arg("peer")
|
||||
.arg(peer_id)
|
||||
.arg("preshared-key")
|
||||
.arg("/dev/stdin")
|
||||
.stdin(Stdio::piped())
|
||||
.args(config.extra_params)
|
||||
.spawn()
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
anyhow::bail!("Could not find wg command");
|
||||
} else {
|
||||
return Err(anyhow::Error::new(e));
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Err(e) = config
|
||||
.psk
|
||||
.store_b64_writer::<MAX_B64_KEY_SIZE, _>(child.stdin.take().unwrap())
|
||||
{
|
||||
error!("could not write psk to wg: {:?}", e);
|
||||
}
|
||||
|
||||
thread::spawn(move || {
|
||||
let status = child.wait();
|
||||
|
||||
if let Ok(status) = status {
|
||||
if status.success() {
|
||||
debug!("successfully passed psk to wg")
|
||||
} else {
|
||||
error!("could not pass psk to wg {:?}", status)
|
||||
}
|
||||
} else {
|
||||
error!("wait failed: {:?}", status)
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WireguardBrokerMio for NativeUnixBroker {
|
||||
type MioError = anyhow::Error;
|
||||
|
||||
fn register(
|
||||
&mut self,
|
||||
_registry: &mio::Registry,
|
||||
_token: mio::Token,
|
||||
) -> Result<(), Self::MioError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_poll(&mut self) -> Result<(), Self::MioError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister(&mut self, _registry: &mio::Registry) -> Result<(), Self::MioError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Builder)]
|
||||
#[builder(pattern = "mutable")]
|
||||
pub struct NativeUnixBrokerConfigBase {
|
||||
pub interface: String,
|
||||
pub peer_id: Public<WG_PEER_LEN>,
|
||||
#[builder(private)]
|
||||
pub extra_params: Vec<u8>,
|
||||
}
|
||||
|
||||
impl NativeUnixBrokerConfigBaseBuilder {
|
||||
pub fn peer_id_b64(
|
||||
&mut self,
|
||||
peer_id: &str,
|
||||
) -> Result<&mut Self, NativeUnixBrokerConfigBaseBuilderError> {
|
||||
let mut peer_id_b64 = Public::<WG_PEER_LEN>::zero();
|
||||
b64_decode(peer_id.as_bytes(), &mut peer_id_b64.value).map_err(|_e| {
|
||||
NativeUnixBrokerConfigBaseBuilderError::ValidationError(
|
||||
"Failed to parse peer id b64".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(self.peer_id(peer_id_b64))
|
||||
}
|
||||
|
||||
pub fn extra_params_ser(
|
||||
&mut self,
|
||||
extra_params: &Vec<String>,
|
||||
) -> Result<&mut Self, NativeUnixBrokerConfigBuilderError> {
|
||||
let params = to_allocvec(extra_params).map_err(|_e| {
|
||||
NativeUnixBrokerConfigBuilderError::ValidationError(
|
||||
"Failed to parse extra params".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(self.extra_params(params))
|
||||
}
|
||||
}
|
||||
|
||||
impl WireguardBrokerCfg for NativeUnixBrokerConfigBase {
|
||||
fn create_config<'a>(&'a self, psk: &'a Secret<WG_KEY_LEN>) -> SerializedBrokerConfig<'a> {
|
||||
SerializedBrokerConfig {
|
||||
interface: self.interface.as_bytes(),
|
||||
peer_id: &self.peer_id,
|
||||
psk,
|
||||
additional_params: &self.extra_params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Builder)]
|
||||
#[builder(pattern = "mutable")]
|
||||
pub struct NativeUnixBrokerConfig<'a> {
|
||||
pub interface: &'a str,
|
||||
pub peer_id: &'a Public<WG_PEER_LEN>,
|
||||
pub psk: &'a Secret<WG_KEY_LEN>,
|
||||
pub extra_params: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<SerializedBrokerConfig<'a>> for NativeUnixBrokerConfig<'a> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: SerializedBrokerConfig<'a>) -> Result<Self, Self::Error> {
|
||||
let iface = std::str::from_utf8(value.interface)
|
||||
.map_err(|_| anyhow::Error::msg("Interface UTF8 decoding error"))?;
|
||||
|
||||
let extra_params: Vec<String> =
|
||||
from_bytes(value.additional_params).map_err(anyhow::Error::new)?;
|
||||
Ok(Self {
|
||||
interface: iface,
|
||||
peer_id: value.peer_id,
|
||||
psk: value.psk,
|
||||
extra_params,
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user