Compare commits

..

2 Commits

Author SHA1 Message Date
wucke13
60155a5530 add first draft of rp rust binary 2023-03-08 22:34:12 +01:00
wucke13
664c591138 transform repo to cargo workspace 2023-03-08 22:34:09 +01:00
129 changed files with 3480 additions and 10467 deletions

View File

@@ -1,200 +0,0 @@
#!/usr/bin/env nu
use log *
# cd to git root
cd (git rev-parse --show-toplevel)
# check if a subject depends on a potential dependency
def depends [
subject:string # package to examine
maybe_dep:string # maybe a dependency of subject
] {
not ( nix why-depends --quiet --derivation $subject $maybe_dep | is-empty )
}
# get attribute names of the attribute set
def get-attr-names [
expr: # nix expression to get attrNames of
] {
nix eval --json $expr --apply builtins.attrNames | from json
}
def job-id [
system:string,
derivation:string,
] {
$"($system)---($derivation)"
}
# map from nixos system to github runner type
let systems_map = {
# aarch64-darwin
# aarch64-linux
i686-linux: ubuntu-latest,
x86_64-darwin: macos-13,
x86_64-linux: ubuntu-latest
}
let targets = (get-attr-names ".#packages"
| par-each {|system| { $system : (get-attr-names $".#packages.($system)") } }
| reduce {|it, acc| $acc | merge $it }
)
mut cachix_workflow = {
name: "Nix",
permissions: {contents: write},
on: {
pull_request: null,
push: {branches: [main]}
},
jobs: {},
}
mut release_workflow = {
name: "Release",
permissions: {contents: write},
on: { push: {tags: ["v*"]} },
jobs: {},
}
let runner_setup = [
{
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 }}"
}
}
]
for system in ($targets | columns) {
if ($systems_map | get -i $system | is-empty) {
log info $"skipping ($system), since there are no GH-Actions runners for it"
continue
}
# lookup the correct runner for $system
let runs_on = [ ($systems_map | get $system) ]
# add jobs for all derivations
let derivations = ($targets | get $system)
for derivation in $derivations {
if ($system == "i686-linux") and ($derivation | str contains "static") {
log info $"skipping ($system).($derivation), due to liboqs 0.8 not present in oqs-sys"
continue
}
if ($system == "i686-linux") and ($derivation | str contains "release-package") {
log info $"skipping ($system).($derivation), due to liboqs 0.8 not present in oqs-sys"
continue
}
# job_id for GH-Actions
let id = ( job-id $system $derivation )
# name displayed
let name = $"($system).($derivation)"
# collection of dependencies
# TODO currently only considers dependencies on the same $system
let needs = ($derivations
| filter {|it| $it != $derivation and $it != "default" } # filter out self and default
| par-each {|it| {
name: $it, # the other derivation
# does self depend on $it?
needed: (depends $".#packages.($system).($derivation)" $".#packages.($system).($it)")
} }
| filter {|it| $it.needed}
| each {|it| job-id $system $it.name}
)
mut new_job = {
name: $"Build ($name)",
"runs-on": $runs_on,
needs: $needs,
steps: ($runner_setup | append [
{
name: Build,
run: $"nix build .#packages.($system).($derivation) --print-build-logs"
}
])
}
$cachix_workflow.jobs = ($cachix_workflow.jobs | insert $id $new_job )
}
# add check job
$cachix_workflow.jobs = ($cachix_workflow.jobs | insert $"($system)---check" {
name: $"Run Nix checks on ($system)",
"runs-on": $runs_on,
steps: ($runner_setup | append {
name: Check,
run: "nix flake check . --print-build-logs"
})
})
# add release job
$release_workflow.jobs = ($release_workflow.jobs | insert $"($system)---release" {
name: $"Build release artifacts for ($system)",
"runs-on": $runs_on,
steps: ($runner_setup | append [
{
name: "Build release",
run: "nix build .#release-package --print-build-logs"
}
{
name: Release,
uses: "softprops/action-gh-release@v1",
with: {
draft: "${{ contains(github.ref_name, 'rc') }}",
prerelease: "${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}",
files: "result/*"
}
}
])
})
}
# add whitepaper job with upload
let system = "x86_64-linux"
$cachix_workflow.jobs = ($cachix_workflow.jobs | insert $"($system)---whitepaper-upload" {
name: $"Upload whitepaper ($system)",
"runs-on": ($systems_map | get $system),
"if": "${{ github.ref == 'refs/heads/main' }}",
steps: ($runner_setup | append [
{
name: "Git add git sha and commit",
run: "cd papers && ./tex/gitinfo2.sh && git add gitHeadInfo.gin"
}
{
name: Build,
run: $"nix build .#packages.($system).whitepaper --print-build-logs"
}
{
name: "Deploy PDF artifacts",
uses: "peaceiris/actions-gh-pages@v3",
with: {
github_token: "${{ secrets.GITHUB_TOKEN }}",
publish_dir: result/,
publish_branch: papers-pdf,
force_orphan: true
}
}
])
})
log info "saving nix-cachix workflow"
$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/

View File

@@ -1,49 +0,0 @@
name: Update website docs
on:
push:
branches:
- main
paths:
- "doc/**"
jobs:
update-website:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Clone rosenpass-website repository
uses: actions/checkout@v3
with:
repository: rosenpass/rosenpass-website
ref: main
path: rosenpass-website
token: ${{ secrets.PRIVACC }}
- name: Copy docs to website repo
run: |
cp -R doc/* rosenpass-website/static/docs/
- name: Install mandoc
run: |
sudo apt-get update
sudo apt-get install -y mandoc
- name: Compile man pages to HTML
run: |
cd rosenpass-website/static/docs/
for file in *.1; do
mandoc -Thtml "$file" > "${file%.*}.html"
done
- name: Commit changes to website repo
uses: EndBug/add-and-commit@v9
with:
author_name: GitHub Actions
author_email: actions@github.com
message: Update docs
cwd: rosenpass-website/static/docs
github_token: ${{ secrets.PRIVACC }

View File

@@ -1,412 +1,74 @@
name: Nix
name: Nix Related Actions
permissions:
contents: write
on:
pull_request: null
pull_request:
push:
branches:
- main
branches: [main]
jobs:
i686-linux---default:
name: Build i686-linux.default
build:
name: Build ${{ matrix.derivation }} on ${{ matrix.nix-system }}
runs-on:
- ubuntu-latest
needs:
- i686-linux---rosenpass
- nix
- ${{ matrix.nix-system }}
strategy:
fail-fast: false
matrix:
nix-system:
- x86_64-linux
# - aarch64-linux
derivation:
- rosenpass
- rosenpass-static
- rosenpass-oci-image
- rosenpass-static-oci-image
- proof-proverif
- whitepaper
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v22
- name: Generate gitHeadInfo.gin for the whitepaper
if: ${{ matrix.derivation == 'whitepaper' }}
run: ( cd papers && ./tex/gitinfo2.sh && git add gitHeadInfo.gin )
- name: Build ${{ matrix.derivation }}@${{ matrix.nix-system }}
run: |
# build the package
nix build .#packages.${{ matrix.nix-system }}.${{ matrix.derivation }} --print-build-logs
# copy over the results
if [[ -f $(readlink --canonicalize result ) ]]; then
mkdir -- ${{ matrix.derivation }}-${{ matrix.nix-system }}
fi
cp --recursive -- $(readlink --canonicalize result) ${{ matrix.derivation }}-${{ matrix.nix-system }}
chmod --recursive ug+rw -- ${{ matrix.derivation }}-${{ matrix.nix-system }}
# add version information
git rev-parse --abbrev-ref HEAD > ${{ matrix.derivation }}-${{ matrix.nix-system }}/git-version
git rev-parse HEAD > ${{ matrix.derivation }}-${{ matrix.nix-system }}/git-sha
# override the `rp` script to keep compatible with non-nix systems
if [[ -f ${{ matrix.derivation }}-${{ matrix.nix-system }}/bin/rp ]]; then
cp --force -- rp ${{ matrix.derivation }}-${{ matrix.nix-system }}/bin/
fi
- name: Upload build results
uses: actions/upload-artifact@v3
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.i686-linux.default --print-build-logs
i686-linux---rosenpass:
name: Build i686-linux.rosenpass
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.i686-linux.rosenpass --print-build-logs
i686-linux---rosenpass-oci-image:
name: Build i686-linux.rosenpass-oci-image
runs-on:
- ubuntu-latest
needs:
- i686-linux---rosenpass
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.i686-linux.rosenpass-oci-image --print-build-logs
i686-linux---check:
name: Run Nix checks on i686-linux
runs-on:
- ubuntu-latest
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: Check
run: nix flake check . --print-build-logs
x86_64-darwin---default:
name: Build x86_64-darwin.default
runs-on:
- macos-13
needs:
- x86_64-darwin---rosenpass
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.default --print-build-logs
x86_64-darwin---release-package:
name: Build x86_64-darwin.release-package
runs-on:
- macos-13
needs:
- x86_64-darwin---rosenpass
- x86_64-darwin---rosenpass-oci-image
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.release-package --print-build-logs
x86_64-darwin---rosenpass:
name: Build x86_64-darwin.rosenpass
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.rosenpass --print-build-logs
x86_64-darwin---rosenpass-oci-image:
name: Build x86_64-darwin.rosenpass-oci-image
runs-on:
- macos-13
needs:
- x86_64-darwin---rosenpass
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.rosenpass-oci-image --print-build-logs
x86_64-darwin---check:
name: Run Nix checks on x86_64-darwin
runs-on:
- macos-13
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: Check
run: nix flake check . --print-build-logs
x86_64-linux---default:
name: Build x86_64-linux.default
runs-on:
- ubuntu-latest
needs:
- x86_64-linux---rosenpass
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.default --print-build-logs
x86_64-linux---proof-proverif:
name: Build x86_64-linux.proof-proverif
runs-on:
- ubuntu-latest
needs:
- x86_64-linux---proverif-patched
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.proof-proverif --print-build-logs
x86_64-linux---proverif-patched:
name: Build x86_64-linux.proverif-patched
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.proverif-patched --print-build-logs
x86_64-linux---release-package:
name: Build x86_64-linux.release-package
runs-on:
- ubuntu-latest
needs:
- x86_64-linux---rosenpass-static-oci-image
- x86_64-linux---rosenpass-static
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.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
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:
- 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.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
x86_64-linux---rosenpass-oci-image:
name: Build x86_64-linux.rosenpass-oci-image
runs-on:
- ubuntu-latest
needs:
- x86_64-linux---rosenpass
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.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:
- 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.rosenpass-static --print-build-logs
x86_64-linux---rosenpass-static-oci-image:
name: Build x86_64-linux.rosenpass-static-oci-image
runs-on:
- ubuntu-latest
needs:
- x86_64-linux---rosenpass-static
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.rosenpass-static-oci-image --print-build-logs
x86_64-linux---whitepaper:
name: Build x86_64-linux.whitepaper
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.whitepaper --print-build-logs
x86_64-linux---check:
name: Run Nix checks on x86_64-linux
runs-on:
- ubuntu-latest
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: Check
run: nix flake check . --print-build-logs
x86_64-linux---whitepaper-upload:
name: Upload whitepaper x86_64-linux
runs-on: ubuntu-latest
if: ${{ github.ref == 'refs/heads/main' }}
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: Git add git sha and commit
run: cd papers && ./tex/gitinfo2.sh && git add gitHeadInfo.gin
- name: Build
run: nix build .#packages.x86_64-linux.whitepaper --print-build-logs
name: ${{ matrix.derivation }}-${{ matrix.nix-system }}
path: ${{ matrix.derivation }}-${{ matrix.nix-system }}
- name: Deploy PDF artifacts
if: ${{ matrix.derivation == 'whitepaper' && github.ref == 'refs/heads/main' }}
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: result/
publish_dir: ${{ matrix.derivation }}-${{ matrix.nix-system }}
publish_branch: papers-pdf
force_orphan: true
checks:
name: Run Nix checks
runs-on: nixos
needs: build
steps:
- uses: actions/checkout@v3
- name: Run Checks
run: nix flake check . --print-build-logs

View File

@@ -1,4 +1,4 @@
name: QC
name: Quality Control
on:
pull_request:
push:
@@ -12,59 +12,15 @@ jobs:
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: actionsx/prettier@v2
with:
args: --check .
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
rustfmt:
name: Rust Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Rust Formatting Script
run: bash format_rust_code.sh --mode check
cargo-bench:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.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
cargo-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
cargo-clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: actions/cache@v3
with:
path: |
@@ -75,104 +31,17 @@ jobs:
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- run: rustup component add clippy
- name: Install libsodium
- name: Install xmllint
run: sudo apt-get install -y libsodium-dev
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
cargo-doc:
cargo-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
- uses: actions/checkout@v1
- uses: actions-rs/audit-check@v1
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
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: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.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
cargo-test-nix-devshell-x86_64-linux:
runs-on:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: cachix/install-nix-action@v21
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v12
with:
name: rosenpass
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- run: nix develop --command cargo test --workspace --all-features
cargo-fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.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
rustup default nightly
- name: Install cargo-fuzz
run: cargo install cargo-fuzz
- name: Run fuzzing
run: |
cargo fuzz run fuzz_aead_enc_into -- -max_total_time=5
cargo fuzz run fuzz_blake2b -- -max_total_time=5
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 -- -max_total_time=5
cargo fuzz run fuzz_vec_secret_alloc -- -max_total_time=5
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -3,69 +3,28 @@ permissions:
contents: write
on:
push:
tags:
- v*
tags: ["v*"]
jobs:
i686-linux---release:
name: Build release artifacts for i686-linux
release:
name: Release for ${{ matrix.nix-system }}
runs-on:
- ubuntu-latest
- nix
- ${{ matrix.nix-system }}
strategy:
fail-fast: false
matrix:
nix-system:
- x86_64-linux
# - aarch64-linux
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 release
- name: Build release-package for ${{ matrix.nix-system }}
run: nix build .#release-package --print-build-logs
- name: Release
uses: softprops/action-gh-release@v1
with:
draft: ${{ contains(github.ref_name, 'rc') }}
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
files: result/*
x86_64-darwin---release:
name: Build release artifacts for x86_64-darwin
runs-on:
- macos-13
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 release
run: nix build .#release-package --print-build-logs
- name: Release
uses: softprops/action-gh-release@v1
with:
draft: ${{ contains(github.ref_name, 'rc') }}
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
files: result/*
x86_64-linux---release:
name: Build release artifacts for x86_64-linux
runs-on:
- ubuntu-latest
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 release
run: nix build .#release-package --print-build-logs
- name: Release
uses: softprops/action-gh-release@v1
with:
draft: ${{ contains(github.ref_name, 'rc') }}
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
files: result/*
files: |
result/*

View File

@@ -1,17 +0,0 @@
# TODO use CI_JOB_TOKEN once https://gitlab.com/groups/gitlab-org/-/epics/6310 is fixed
pull-from-gh:
only: ["schedules"]
variables:
REMOTE: "https://github.com/rosenpass/rosenpass.git"
LOCAL: " git@gitlab.com:rosenpass/rosenpass.git"
GIT_STRATEGY: none
before_script:
- mkdir ~/.ssh/
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- echo "$REPO_SSH_KEY" > ~/.ssh/id_ed25519
- chmod 600 --recursive ~/.ssh/
- git config --global user.email "ci@gitlab.com"
- git config --global user.name "CI"
script:
- git clone --mirror $REMOTE rosenpass
- cd rosenpass && git push --mirror $LOCAL

1537
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +1,6 @@
[workspace]
resolver = "2"
members = [
"rosenpass",
"cipher-traits",
"ciphers",
"util",
"constant-time",
"oqs",
"to",
"fuzz",
"secret-memory",
]
default-members = [
"rosenpass"
]
[workspace.metadata.release]
# ensure that adding `--package` as argument to `cargo release` still creates version tags in the form of `vx.y.z`
tag-prefix = ""
[workspace.dependencies]
rosenpass = { path = "rosenpass" }
rosenpass-util = { path = "util" }
rosenpass-constant-time = { path = "constant-time" }
rosenpass-cipher-traits = { path = "cipher-traits" }
rosenpass-ciphers = { path = "ciphers" }
rosenpass-to = { path = "to" }
rosenpass-secret-memory = { path = "secret-memory" }
rosenpass-oqs = { path = "oqs" }
criterion = "0.4.0"
test_bin = "0.4.0"
libfuzzer-sys = "0.4"
stacker = "0.1.15"
doc-comment = "0.3.3"
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"
allocator-api2-tests = "0.2.14"
memsec = "0.6.3"
rand = "0.8.5"
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" ] }
zerocopy = { version = "0.7.32", features = ["derive"] }
home = "0.5.9"
"rp",
]

View File

@@ -3,33 +3,12 @@
#define SESSION_START_EVENTS 0
#define RANDOMIZED_CALL_IDS 0
#include "config.mpv"
#include "prelude/basic.mpv"
#include "crypto/key.mpv"
#include "crypto/kem.mpv"
#include "rosenpass/oracles.mpv"
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].
let main = rosenpass_main.
@lemma "state coherence, initiator: Initiator accepting a RespHello message implies they also generated the associated InitHello message"

View File

@@ -10,26 +10,6 @@
let main = rosenpass_main.
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].
@lemma "non-interruptability: Adv cannot prevent a genuine InitHello message from being accepted"
lemma ih:InitHello_t, psk:key, sski:kem_sk, sskr:kem_sk;
event(IHRjct(ih, psk, sskr, kem_pub(sski)))

View File

@@ -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].

View File

@@ -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].

View File

@@ -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.

View File

@@ -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)).

View File

@@ -88,18 +88,6 @@ set verboseCompleted=VERBOSE.
#define SES_EV(...)
#endif
#if COOKIE_EVENTS
#define COOKIE_EV(...) __VA_ARGS__
#else
#define COOKIE_EV(...)
#endif
#if KEM_EVENTS
#define KEM_EV(...) __VA_ARGS__
#else
#define KEM_EV(...)
#endif
(* TODO: Authentication timing properties *)
(* TODO: Proof that every adversary submitted package is equivalent to one generated by the proper algorithm using different coins. This probably requires introducing an oracle that extracts the coins used and explicitly adding the notion of coins used for Packet->Packet steps and an inductive RNG notion. *)

View File

@@ -1,155 +0,0 @@
/*
This identity hiding process tests whether the rosenpass protocol is able to protect the identity of an initiator or responder.
The participants in the test are trusted initiators, trusted responders and compromised initiators and responders.
The test consists of two phases. In the first phase all of the participants can communicate with each other using the rosenpass protocol.
An attacker observes the first phase and is able to intercept and modify messages and choose participants to communicate with each other
In the second phase if the anonymity of an initiator is being tested then one of two trusted initiators is chosen.
The chosen initiator communicates directly with a trusted responder.
If an attacker can determine which initiator was chosen then the anonymity of the initiator has been compromised.
Otherwise the protocol has successfully protected the initiators identity.
If the anonymity of a responder is being tested then one of two trusted responders is chosen instead.
Then an initiator communicates directly with the chosen responder.
If an attacker can determine which responder was chosen then the anonymity of the responder is compromised.
Otherwise the protocol successfully protects the identity of a responder.
The Proverif code treats the public key as synonymous with identity.
In the above test when a responder or initiator is chosen what is actually chosen is the public/private key pair to use for communication.
Traditionally when a responder or initiator is chosen they would be chosen randomly.
The way Proverif makes a "choice" is by simulating multiple processes, one process per choice
Then the processes are compared and if an association between a public key and a process can be made the test fails.
As the choice is at least as bad as choosing the worst possible option the credibility of the test is maintained.
The drawback is that Proverif is only able to tell if the identity can be brute forced but misses any probabilistic associations.
As usual Proverif also assumes perfect encryption and in particular assumes encryption cannot be linked to identity.
One of the tradeoffs made here is that the choice function in Proverif is slow but this is in favour of being able to write more precise tests.
Another issue is the choice function does not work with queries so a test needs to be run for each set of assumptions.
In this case the test uses secure rng and a fresh secure biscuit key.
*/
#include "config.mpv"
#define CHAINING_KEY_EVENTS 1
#define MESSAGE_TRANSMISSION_EVENTS 1
#define SESSION_START_EVENTS 0
#define RANDOMIZED_CALL_IDS 0
#undef FULL_MODEL
#undef SIMPLE_MODEL
#define SIMPLE_MODEL 1
#include "prelude/basic.mpv"
#include "crypto/key.mpv"
#include "rosenpass/oracles.mpv"
#include "crypto/kem.mpv"
#define NEW_TRUSTED_SEED(name) \
new MCAT(name, _secret_seed):seed_prec; \
name <- make_trusted_seed(MCAT(name, _secret_seed)); \
free D:channel [private].
free secure_biscuit_no:Atom [private].
free secure_sidi,secure_sidr:SessionId [private].
free secure_psk:key [private].
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));
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).
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));
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, 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 pipeChannel(D:channel, C:channel) =
in(D, b:bits);
out(C, b).
let secretCommunication() =
#ifdef INITIATOR_TEST
initiator_seed <- choice[make_trusted_kem_sk(initiator1), make_trusted_kem_sk(initiator2)];
#else
initiator_seed <- make_trusted_kem_sk(initiator1);
#endif
#ifdef RESPONDER_TEST
responder_seed <- choice[make_trusted_kem_sk(responder1), make_trusted_kem_sk(responder2)];
#else
responder_seed <- make_trusted_kem_sk(responder1);
#endif
secure_communication(initiator_seed, responder_seed, secure_psk) | !pipeChannel(D, C).
let reveal_pks() =
out(C, setup_kem_pk(make_trusted_kem_sk(responder1)));
out(C, setup_kem_pk(make_trusted_kem_sk(responder2)));
out(C, setup_kem_pk(make_trusted_kem_sk(initiator1)));
out(C, setup_kem_pk(make_trusted_kem_sk(initiator2))).
let rosenpass_main2() =
REP(INITIATOR_BOUND, Oinitiator)
| REP(RESPONDER_BOUND, Oinit_hello)
| REP(RESPONDER_BOUND, Oinit_conf).
let identity_hiding_main() =
0 | reveal_pks() | rosenpass_main2() | participants_communication() | phase 1; secretCommunication().
#ifndef CUSTOM_MAIN
let main = identity_hiding_main.
#endif

View File

@@ -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).)

View File

@@ -41,32 +41,23 @@ restriction s:seed, p1:Atom, p2:Atom, ad1:Atom, ad2:Atom;
event(ConsumeSeed(p1, s, ad1)) && event(ConsumeSeed(p2, s, ad2))
==> p1 = p2 && ad1 = ad2.
letfun create_mac2(k:key, msg:bits) = prf(k,msg).
#include "rosenpass/responder.macro"
fun Cinit_conf(kem_sk_tmpl, key_tmpl, kem_pk_tmpl, InitConf_t) : Atom [data].
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). )
KEM_EV(event Oinit_conf_KemUse(SessionId, SessionId, Atom).)
#ifdef KEM_EVENTS
restriction sidi:SessionId, sidr:SessionId, ad1:Atom, ad2:Atom;
event(Oinit_conf_KemUse(sidi, sidr, ad1)) && event(Oinit_conf_KemUse(sidi, sidr, ad2))
==> ad1 = ad2.
#endif
event ConsumeBiscuit(Atom, kem_sk, kem_pk, Atom).
fun Ccookie(key, bits) : Atom[data].
let Oinit_conf_inner(Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, ic:InitConf_t, call:Atom) =
let Oinit_conf() =
in(C, Cinit_conf(Ssskm, Spsk, Sspkt, ic));
#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_ = (
let InitConf(sidi, sidr, biscuit, auth) = ic in
KEM_EV(event Oinit_conf_KemUse(sidi, sidr, call);)
INITCONF_CONSUME()
event ConsumeBiscuit(biscuit_no, sskm, spkt, call);
CK_EV( event OskOinit_conf(ck_rh, osk); )
@@ -81,21 +72,11 @@ let Oinit_conf_inner(Ssskm:kem_sk_tmpl, Spsk:key_tmpl, Sspkt:kem_sk_tmpl, ic:Ini
0
#endif
).
let Oinit_conf() =
in(C, Cinit_conf(Ssskm, Spsk, Sspkt, ic));
#if RANDOMIZED_CALL_IDS
new call:Atom;
#else
call <- Cinit_conf(Ssskm, Spsk, Sspkt, ic);
#endif
Oinit_conf_inner(Ssskm, Spsk, Sspkt, ic, call).
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))
==> ad1 = ad2.
// TODO: Restriction biscuit no invalidation
#include "rosenpass/initiator.macro"
@@ -104,56 +85,27 @@ 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). )
KEM_EV(event Oresp_hello_KemUse(SessionId, SessionId, Atom).)
#ifdef KEM_EVENTS
restriction sidi:SessionId, sidr:SessionId, ad1:Atom, ad2:Atom;
event(Oresp_hello_KemUse(sidi, sidr, ad1)) && event(Oresp_hello_KemUse(sidi, sidr, ad2))
==> ad1 = ad2.
#endif
#ifdef COOKIE_EVENTS
COOKIE_EVENTS(Oresp_hello)
#endif
let Oresp_hello(HS_DECL_ARGS, C_in:channel, call:Atom) =
in(C_in, Cresp_hello(RespHello(sidr, =sidi, ecti, scti, biscuit, auth)));
in(C_in, mac2_key:key);
let Oresp_hello(HS_DECL_ARGS) =
in(C, Cresp_hello(RespHello(sidr, =sidi, ecti, scti, biscuit, auth)));
rh <- RespHello(sidr, sidi, ecti, scti, biscuit, auth);
#ifdef COOKIE_EVENTS
msg <- RH2b(rh);
COOKIE_PROCESS(Oresp_hello,
#endif
/* try */ let ic = (
ck_ini <- ck;
KEM_EV(event Oresp_hello_KemUse(sidi, sidr, call);)
RESPHELLO_CONSUME()
ck_ih <- ck;
INITCONF_PRODUCE()
CK_EV (event OskOresp_hello(ck_ini, ck_ih, osk); ) // TODO: Queries testing that there is no duplication
MTX_EV( event ICSent(rh, ic, psk, sski, spkr); )
SES_EV( event InitiatorSession(rh, osk); )
ic
/* success */ ) in (
icbits <- IC2b(ic);
mac <- create_mac(spkt, icbits);
mac2 <- create_mac2(mac2_key, mac_envelope2b(mac));
out(C_in, ic);
out(C_in, mac);
out(C_in, mac2)
/* fail */ ) else (
#if MESSAGE_TRANSMISSION_EVENTS
event RHRjct(rh, psk, sski, spkr)
#else
0
#endif
)
#ifdef COOKIE_EVENTS
)
/* try */ let ic = (
ck_ini <- ck;
RESPHELLO_CONSUME()
ck_ih <- ck;
INITCONF_PRODUCE()
CK_EV (event OskOresp_hello(ck_ini, ck_ih, osk); ) // TODO: Queries testing that there is no duplication
MTX_EV( event ICSent(rh, ic, psk, sski, spkr); )
SES_EV( event InitiatorSession(rh, osk); )
ic
/* success */ ) in (
out(C, ic)
/* fail */ ) else (
#if MESSAGE_TRANSMISSION_EVENTS
event RHRjct(rh, psk, sski, spkr)
#else
.
0
#endif
).
// TODO: Restriction: Biscuit no invalidation
@@ -164,33 +116,24 @@ 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).
KEM_EV(event Oinit_hello_KemUse(SessionId, SessionId, Atom).)
#ifdef KEM_EVENTS
restriction sidi:SessionId, sidr:SessionId, ad1:Atom, ad2:Atom;
event(Oinit_hello_KemUse(sidi, sidr, ad1)) && event(Oinit_hello_KemUse(sidi, sidr, ad2))
==> ad1 = ad2.
let Oinit_hello() =
in(C, Cinit_hello(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih));
#if RANDOMIZED_CALL_IDS
new call:Atom;
#else
call <- Cinit_hello(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih);
#endif
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, mac2_key:key, C_out:channel, call:Atom) =
// TODO: This is ugly
let InitHello(sidi, epki, sctr, pidiC, auth) = ih in
SETUP_HANDSHAKE_STATE()
eski <- kem_sk0;
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 ConsumeBn(biscuit_no, sskm, spkt, call);
event ConsumeSidr(sidr, call);
event ConsumeSeed(Epti, setup_seed(Septi), call);
event ConsumeSeed(Spti, setup_seed(Sspti), call);
// out(C_out, spkt);
let rh = (
KEM_EV(event Oinit_hello_KemUse(sidi, sidr, call);)
INITHELLO_CONSUME()
ck_ini <- ck;
RESPHELLO_PRODUCE()
@@ -198,14 +141,7 @@ let Oinit_hello_inner(sidm:SessionId, biscuit_no:Atom, Ssskm:kem_sk_tmpl, Spsk:k
MTX_EV( event RHSent(ih, rh, psk, sskr, spki); )
rh
/* success */ ) in (
rhbits <- RH2b(rh);
mac <- create_mac(spkt, rhbits);
out(C_out, rh);
out(C_out, mac);
mac2 <- create_mac2(mac2_key, mac_envelope2b(mac));
out(C_out, mac2)
out(C, rh)
/* fail */ ) else (
#if MESSAGE_TRANSMISSION_EVENTS
event IHRjct(ih, psk, sskr, spki)
@@ -214,18 +150,6 @@ let Oinit_hello_inner(sidm:SessionId, biscuit_no:Atom, Ssskm:kem_sk_tmpl, Spsk:k
#endif
).
let Oinit_hello() =
in(C, Cinit_hello(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih));
in(C, mac2_key:key);
#if RANDOMIZED_CALL_IDS
new call:Atom;
#else
call <- Cinit_hello(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih);
#endif
Oinit_hello_inner(sidr, biscuit_no, Ssskm, Spsk, Sspkt, Septi, Sspti, ih, mac2_key, C, call).
restriction sid:SessionId, ad1:Atom, ad2:Atom;
event(ConsumeSidr(sid, ad1)) && event(ConsumeSidr(sid, ad2))
==> ad1 = ad2.
@@ -242,55 +166,27 @@ fun Cinitiator(SessionId, kem_sk_tmpl, key_tmpl, kem_pk_tmpl, seed_tmpl, seed_tm
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). )
KEM_EV(event Oinitiator_inner_KemUse(SessionId, SessionId, Atom).)
#ifdef KEM_EVENTS
restriction sidi:SessionId, sidr:SessionId, ad1:Atom, ad2:Atom;
event(Oinitiator_inner_KemUse(sidi, sidr, ad1)) && event(Oinitiator_inner_KemUse(sidi, sidr, ad2))
==> ad1 = ad2.
#endif
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, last_cookie:key, C_out:channel, call:Atom) =
SETUP_HANDSHAKE_STATE()
sidr <- sid0;
KEM_EV(event Oinitiator_inner_KemUse(sidi, sidr, call);)
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, ih);
ihbits <- IH2b(ih);
mac <- create_mac(spkt, ihbits);
out(C_out, mac);
mac2 <- create_mac2(last_cookie, mac_envelope2b(mac));
out(C_out, mac2);
Oresp_hello(HS_PASS_ARGS, C_out, call).
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
in(C, last_cookie:key);
Oinitiator_inner(sidi, Ssskm, Spsk, Sspkt, Seski, Ssptr, last_cookie, C, call).
#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).
restriction sid:SessionId, ad1:Atom, ad2:Atom;
event(ConsumeSidi(sid, ad1)) && event(ConsumeSidi(sid, ad2))
@@ -311,3 +207,21 @@ let rosenpass_main() = 0
| REP(RESPONDER_BOUND, Oinit_hello)
| REP(RESPONDER_BOUND, Oinit_conf).
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].

View File

@@ -2,26 +2,6 @@
#include "crypto/kem.mpv"
#include "rosenpass/handshake_state.mpv"
fun Envelope(
key,
bits
): bits [data].
type mac_envelope_t.
fun mac_envelope(
key,
bits
) : mac_envelope_t.
fun mac_envelope2b(mac_envelope_t) : bits [typeConverter].
letfun create_mac(pk:kem_pk, payload:bits) = mac_envelope(lprf2(MAC, kem_pk2b(pk), payload), payload).
fun mac_envelope_pk_test(mac_envelope_t, kem_pk) : bool
reduc forall pk:kem_pk, b:bits;
mac_envelope_pk_test(mac_envelope(prf(prf(prf(prf(key0,PROTOCOL),MAC),kem_pk2b(pk)),
b), b), pk) = true.
type InitHello_t.
fun InitHello(
SessionId, // sidi
@@ -31,8 +11,6 @@ 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 */ \
@@ -63,9 +41,7 @@ fun RespHello(
bits // auth
) : RespHello_t [data].
fun RH2b(RespHello_t) : bitstring [typeConverter].
#define RESPHELLO_PRODUCE() \
#define RESPHELLO_PRODUCE() \
/* not handled here */ /* RHR1 */ \
MIX2(sid2b(sidr), sid2b(sidi)) /* RHR3 */ \
ENCAPS_AND_MIX(ecti, epki, epti) /* RHR4 */ \
@@ -91,14 +67,13 @@ 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 */ \
ic <- InitConf(sidi, sidr, biscuit, auth);
#define INITCONF_CONSUME() \
let InitConf(sidi, sidr, biscuit, auth) = ic in \
LOAD_BISCUIT(biscuit_no, biscuit) /* ICR1 */ \
ENCRYPT_AND_MIX(rh_auth, empty) /* ICIR */ \
ck_rh <- ck; /* ---- */ /* TODO: Move into oracles.mpv */ \

View File

@@ -1,12 +0,0 @@
[package]
name = "rosenpass-cipher-traits"
authors = ["Karolin Varner <karo@cupdev.net>", "wucke13 <wucke13@gmail.com>"]
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Rosenpass internal traits for cryptographic primitives"
homepage = "https://rosenpass.eu/"
repository = "https://github.com/rosenpass/rosenpass"
readme = "readme.md"
[dependencies]

View File

@@ -1,5 +0,0 @@
# Rosenpass internal libsodium bindings
Rosenpass internal library providing traits for cryptographic primitives.
This is an internal library; not guarantee is made about its API at this point in time.

View File

@@ -1,47 +0,0 @@
//! Traits and implementations for Key Encapsulation Mechanisms (KEMs)
//!
//! KEMs are the interface provided by almost all post-quantum
//! secure key exchange mechanisms.
//!
//! Conceptually KEMs are akin to public-key encryption, but instead of encrypting
//! arbitrary data, KEMs are limited to the transmission of keys, randomly chosen during
//!
//! encapsulation.
//! The [KEM] Trait describes the basic API offered by a Key Encapsulation
//! Mechanism. Two implementations for it are provided, [StaticKEM] and [EphemeralKEM].
use std::result::Result;
/// Key Encapsulation Mechanism
///
/// The KEM interface defines three operations: Key generation, key encapsulation and key
/// decapsulation.
pub trait Kem {
type Error;
/// Secrete Key length
const SK_LEN: usize;
/// Public Key length
const PK_LEN: usize;
/// Ciphertext length
const CT_LEN: usize;
/// Shared Secret length
const SHK_LEN: usize;
/// Generate a keypair consisting of secret key (`sk`) and public key (`pk`)
///
/// `keygen() -> sk, pk`
fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), Self::Error>;
/// From a public key (`pk`), generate a shared key (`shk`, for local use)
/// and a cipher text (`ct`, to be sent to the owner of the `pk`).
///
/// `encaps(pk) -> shk, ct`
fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), Self::Error>;
/// From a secret key (`sk`) and a cipher text (`ct`) derive a shared key
/// (`shk`)
///
/// `decaps(sk, ct) -> shk`
fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), Self::Error>;
}

View File

@@ -1,2 +0,0 @@
mod kem;
pub use kem::Kem;

View File

@@ -1,22 +0,0 @@
[package]
name = "rosenpass-ciphers"
authors = ["Karolin Varner <karo@cupdev.net>", "wucke13 <wucke13@gmail.com>"]
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Rosenpass internal ciphers and other cryptographic primitives used by rosenpass."
homepage = "https://rosenpass.eu/"
repository = "https://github.com/rosenpass/rosenpass"
readme = "readme.md"
[dependencies]
anyhow = { workspace = true }
rosenpass-to = { workspace = true }
rosenpass-constant-time = { workspace = true }
rosenpass-secret-memory = { workspace = true }
rosenpass-oqs = { workspace = true }
rosenpass-util = { workspace = true }
static_assertions = { workspace = true }
zeroize = { workspace = true }
chacha20poly1305 = { workspace = true }
blake2 = { workspace = true }

View File

@@ -1,5 +0,0 @@
# Rosenpass internal cryptographic primitives
Ciphers and other cryptographic primitives used by rosenpass.
This is an internal library; not guarantee is made about its API at this point in time.

View File

@@ -1,109 +0,0 @@
use anyhow::Result;
use rosenpass_secret_memory::Secret;
use rosenpass_to::To;
use crate::subtle::incorrect_hmac_blake2b as hash;
pub use hash::KEY_LEN;
// TODO Use a proper Dec interface
#[derive(Clone, Debug)]
pub struct HashDomain([u8; KEY_LEN]);
#[derive(Clone, Debug)]
pub struct HashDomainNamespace([u8; KEY_LEN]);
#[derive(Clone, Debug)]
pub struct SecretHashDomain(Secret<KEY_LEN>);
#[derive(Clone, Debug)]
pub struct SecretHashDomainNamespace(Secret<KEY_LEN>);
impl HashDomain {
pub fn zero() -> Self {
Self([0u8; KEY_LEN])
}
pub fn dup(self) -> HashDomainNamespace {
HashDomainNamespace(self.0)
}
pub fn turn_secret(self) -> SecretHashDomain {
SecretHashDomain(Secret::from_slice(&self.0))
}
// TODO: Protocol! Use domain separation to ensure that
pub fn mix(self, v: &[u8]) -> Result<Self> {
Ok(Self(hash::hash(&self.0, v).collect::<[u8; KEY_LEN]>()?))
}
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretHashDomain> {
SecretHashDomain::invoke_primitive(&self.0, v.secret())
}
pub fn into_value(self) -> [u8; KEY_LEN] {
self.0
}
}
impl HashDomainNamespace {
pub fn mix(&self, v: &[u8]) -> Result<HashDomain> {
Ok(HashDomain(
hash::hash(&self.0, v).collect::<[u8; KEY_LEN]>()?,
))
}
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretHashDomain> {
SecretHashDomain::invoke_primitive(&self.0, v.secret())
}
}
impl SecretHashDomain {
pub fn invoke_primitive(k: &[u8], d: &[u8]) -> Result<SecretHashDomain> {
let mut r = SecretHashDomain(Secret::zero());
hash::hash(k, d).to(r.0.secret_mut())?;
Ok(r)
}
pub fn zero() -> Self {
Self(Secret::zero())
}
pub fn dup(self) -> SecretHashDomainNamespace {
SecretHashDomainNamespace(self.0)
}
pub fn danger_from_secret(k: Secret<KEY_LEN>) -> Self {
Self(k)
}
pub fn mix(self, v: &[u8]) -> Result<SecretHashDomain> {
Self::invoke_primitive(self.0.secret(), v)
}
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretHashDomain> {
Self::invoke_primitive(self.0.secret(), v.secret())
}
pub fn into_secret(self) -> Secret<KEY_LEN> {
self.0
}
pub fn into_secret_slice(mut self, v: &[u8], dst: &[u8]) -> Result<()> {
hash::hash(v, dst).to(self.0.secret_mut())
}
}
impl SecretHashDomainNamespace {
pub fn mix(&self, v: &[u8]) -> Result<SecretHashDomain> {
SecretHashDomain::invoke_primitive(self.0.secret(), v)
}
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretHashDomain> {
SecretHashDomain::invoke_primitive(self.0.secret(), v.secret())
}
// TODO: This entire API is not very nice; we need this for biscuits, but
// it might be better to extract a special "biscuit"
// labeled subkey and reinitialize the chain with this
pub fn danger_into_secret(self) -> Secret<KEY_LEN> {
self.0
}
}

View File

@@ -1,27 +0,0 @@
use static_assertions::const_assert;
pub mod subtle;
pub const KEY_LEN: usize = 32;
const_assert!(KEY_LEN == aead::KEY_LEN);
const_assert!(KEY_LEN == xaead::KEY_LEN);
const_assert!(KEY_LEN == hash_domain::KEY_LEN);
/// Authenticated encryption with associated data
pub mod aead {
pub use crate::subtle::chacha20poly1305_ietf::{decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN};
}
/// Authenticated encryption with associated data with a constant nonce
pub mod xaead {
pub use crate::subtle::xchacha20poly1305_ietf::{
decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN,
};
}
pub mod hash_domain;
pub mod kem {
pub use rosenpass_oqs::ClassicMceliece460896 as StaticKem;
pub use rosenpass_oqs::Kyber512 as EphemeralKem;
}

View File

@@ -1,42 +0,0 @@
use zeroize::Zeroizing;
use blake2::digest::crypto_common::generic_array::GenericArray;
use blake2::digest::crypto_common::typenum::U32;
use blake2::digest::crypto_common::KeySizeUser;
use blake2::digest::{FixedOutput, Mac, OutputSizeUser};
use blake2::Blake2bMac;
use rosenpass_to::{ops::copy_slice, with_destination, To};
use rosenpass_util::typenum2const;
type Impl = Blake2bMac<U32>;
type KeyLen = <Impl as KeySizeUser>::KeySize;
type OutLen = <Impl as OutputSizeUser>::OutputSize;
const KEY_LEN: usize = typenum2const! { KeyLen };
const OUT_LEN: usize = typenum2const! { OutLen };
pub const KEY_MIN: usize = KEY_LEN;
pub const KEY_MAX: usize = KEY_LEN;
pub const OUT_MIN: usize = OUT_LEN;
pub const OUT_MAX: usize = OUT_LEN;
#[inline]
pub fn hash<'a>(key: &'a [u8], data: &'a [u8]) -> impl To<[u8], anyhow::Result<()>> + 'a {
with_destination(|out: &mut [u8]| {
let mut h = Impl::new_from_slice(key)?;
h.update(data);
// Jesus christ, blake2 crate, your usage of GenericArray might be nice and fancy
// but it introduces a ton of complexity. This cost me half an hour just to figure
// 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 mut tmp = GenericArray::from_mut_slice(tmp.as_mut());
h.finalize_into(&mut tmp);
copy_slice(tmp.as_ref()).to(out);
Ok(())
})
}

View File

@@ -1,43 +0,0 @@
use rosenpass_to::ops::copy_slice;
use rosenpass_to::To;
use rosenpass_util::typenum2const;
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::ChaCha20Poly1305 as AeadImpl;
use chacha20poly1305::{AeadCore, AeadInPlace, KeyInit, KeySizeUser};
pub const KEY_LEN: usize = typenum2const! { <AeadImpl as KeySizeUser>::KeySize };
pub const TAG_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::TagSize };
pub const NONCE_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::NonceSize };
#[inline]
pub fn encrypt(
ciphertext: &mut [u8],
key: &[u8],
nonce: &[u8],
ad: &[u8],
plaintext: &[u8],
) -> anyhow::Result<()> {
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)?;
copy_slice(&mac_value[..]).to(mac);
Ok(())
}
#[inline]
pub fn decrypt(
plaintext: &mut [u8],
key: &[u8],
nonce: &[u8],
ad: &[u8],
ciphertext: &[u8],
) -> anyhow::Result<()> {
let nonce = GenericArray::from_slice(nonce);
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)?;
Ok(())
}

View File

@@ -1,46 +0,0 @@
use anyhow::ensure;
use zeroize::Zeroizing;
use rosenpass_constant_time::xor;
use rosenpass_to::{ops::copy_slice, with_destination, To};
use crate::subtle::blake2b;
pub const KEY_LEN: usize = 32;
pub const KEY_MIN: usize = KEY_LEN;
pub const KEY_MAX: usize = KEY_LEN;
pub const OUT_MIN: usize = blake2b::OUT_MIN;
pub const OUT_MAX: usize = blake2b::OUT_MAX;
/// This is a woefully incorrect implementation of hmac_blake2b.
/// See <https://github.com/rosenpass/rosenpass/issues/68#issuecomment-1563612222>
///
/// It accepts 32 byte keys, exclusively.
///
/// This will be replaced, likely by Kekkac at some point soon.
/// <https://github.com/rosenpass/rosenpass/pull/145>
#[inline]
pub fn hash<'a>(key: &'a [u8], data: &'a [u8]) -> impl To<[u8], anyhow::Result<()>> + 'a {
const IPAD: [u8; KEY_LEN] = [0x36u8; KEY_LEN];
const OPAD: [u8; KEY_LEN] = [0x5Cu8; KEY_LEN];
with_destination(|out: &mut [u8]| {
// Not bothering with padding; the implementation
// uses appropriately sized keys.
ensure!(key.len() == KEY_LEN);
type Key = Zeroizing<[u8; KEY_LEN]>;
let mut tmp_key = Key::default();
copy_slice(key).to(tmp_key.as_mut());
xor(&IPAD).to(tmp_key.as_mut());
let mut outer_data = Key::default();
blake2b::hash(tmp_key.as_ref(), data).to(outer_data.as_mut())?;
copy_slice(key).to(tmp_key.as_mut());
xor(&OPAD).to(tmp_key.as_mut());
blake2b::hash(tmp_key.as_ref(), outer_data.as_ref()).to(out)?;
Ok(())
})
}

View File

@@ -1,4 +0,0 @@
pub mod blake2b;
pub mod chacha20poly1305_ietf;
pub mod incorrect_hmac_blake2b;
pub mod xchacha20poly1305_ietf;

View File

@@ -1,45 +0,0 @@
use rosenpass_to::ops::copy_slice;
use rosenpass_to::To;
use rosenpass_util::typenum2const;
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::XChaCha20Poly1305 as AeadImpl;
use chacha20poly1305::{AeadCore, AeadInPlace, KeyInit, KeySizeUser};
pub const KEY_LEN: usize = typenum2const! { <AeadImpl as KeySizeUser>::KeySize };
pub const TAG_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::TagSize };
pub const NONCE_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::NonceSize };
#[inline]
pub fn encrypt(
ciphertext: &mut [u8],
key: &[u8],
nonce: &[u8],
ad: &[u8],
plaintext: &[u8],
) -> anyhow::Result<()> {
let nonce = GenericArray::from_slice(nonce);
let (n, ct_mac) = ciphertext.split_at_mut(NONCE_LEN);
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)?;
copy_slice(&mac_value[..]).to(mac);
Ok(())
}
#[inline]
pub fn decrypt(
plaintext: &mut [u8],
key: &[u8],
ad: &[u8],
ciphertext: &[u8],
) -> anyhow::Result<()> {
let (n, ct_mac) = ciphertext.split_at(NONCE_LEN);
let (ct, mac) = ct_mac.split_at(ct_mac.len() - TAG_LEN);
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)?;
Ok(())
}

View File

@@ -1,2 +0,0 @@
peer-*-*-key
peer-*-out

View File

@@ -1,18 +0,0 @@
public_key = "peer-a-public-key"
secret_key = "peer-a-secret-key"
listen = ["[::]:10001"]
verbosity = "Quiet"
[[peers]]
public_key = "peer-b-public-key"
endpoint = "localhost:10002"
key_out = "peer-a-rp-out-key"
# exchange_command = [
# "wg",
# "set",
# "wg0",
# "peer",
# "<PEER_ID>",
# "preshared-key",
# "/dev/stdin",
# ]

View File

@@ -1,18 +0,0 @@
public_key = "peer-b-public-key"
secret_key = "peer-b-secret-key"
listen = ["[::]:10002"]
verbosity = "Quiet"
[[peers]]
public_key = "peer-a-public-key"
endpoint = "localhost:10001"
key_out = "peer-b-rp-out-key"
# exchange_command = [
# "wg",
# "set",
# "wg0",
# "peer",
# "<PEER_ID>",
# "preshared-key",
# "/dev/stdin",
# ]

View File

@@ -1,22 +0,0 @@
[package]
name = "rosenpass-constant-time"
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 utilities for constant time crypto implementations"
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
[features]
constant_time_tests = []
[dependencies]
rosenpass-to = { workspace = true }
memsec = { workspace = true }
[dev-dependencies]
rand = "0.8.5"

View File

@@ -1,5 +0,0 @@
# Rosenpass constant time library
Rosenpass internal library providing basic constant-time operations.
This is an internal library; not guarantee is made about its API at this point in time.

View File

@@ -1,42 +0,0 @@
/// Compares two slices of memory containing arbitrary-length little endian unsigned integers
/// and returns an integer indicating the relationship between the slices.
///
/// ## Returns
///
/// - -1 if a < b
/// - 0 if a = b
/// - 1 if a > 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
///
/// ```rust
/// use rosenpass_constant_time::compare;
/// assert_eq!(compare(&[], &[]), 0);
///
/// assert_eq!(compare(&[0], &[1]), -1);
/// assert_eq!(compare(&[0], &[0]), 0);
/// assert_eq!(compare(&[1], &[0]), 1);
///
/// assert_eq!(compare(&[0, 0], &[1, 0]), -1);
/// assert_eq!(compare(&[0, 0], &[0, 0]), 0);
/// assert_eq!(compare(&[1, 0], &[0, 0]), 1);
///
/// assert_eq!(compare(&[1, 0], &[0, 1]), -1);
/// assert_eq!(compare(&[0, 1], &[0, 0]), 1);
/// ```
///
/// 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 { memsec::memcmp(a.as_ptr(), b.as_ptr(), a.len()) }
}

View File

@@ -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);
}
}

View File

@@ -1,17 +0,0 @@
//! 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>
mod compare;
mod increment;
mod memcmp;
mod xor;
pub use compare::compare;
pub use increment::increment;
pub use memcmp::memcmp;
pub use xor::xor;

View File

@@ -1,110 +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() as *const u8, b.as_ptr() as *const u8, 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"
)
}
}

View File

@@ -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);
}
})
}

View File

@@ -1,114 +0,0 @@
.Dd $Mdocdate$
.Dt ROSENPASS 1
.Os
.Sh NAME
.Nm rosenpass
.Nd builds post-quantum-secure VPNs
.Sh SYNOPSIS
.Nm
.Op COMMAND
.Op Ar OPTIONS ...
.Op Ar ARGS ...
.Sh DESCRIPTION
.Nm
performs cryptographic key exchanges that are secure against quantum-computers
and then outputs the keys.
These keys can then be passed to various services, such as wireguard or other
vpn services, as pre-shared-keys to achieve security against attackers with
quantum computers.
.Pp
This is a research project and quantum computers are not thought to become
practical in fewer than ten years.
If you are not specifically tasked with developing post-quantum secure systems,
you probably do not need this tool.
.Ss COMMANDS
.Bl -tag -width Ds
.It Ar gen-keys --secret-key <file-path> --public-key <file-path>
Generate a keypair to use in the exchange command later.
Send the public-key file to your communication partner and keep the private-key
file secret!
.It Ar exchange private-key <file-path> public-key <file-path> [ OPTIONS ] PEERS
Start a process to exchange keys with the specified peers.
You should specify at least one peer.
.Pp
Its
.Ar OPTIONS
are as follows:
.Bl -tag -width Ds
.It Ar listen <ip>[:<port>]
Instructs
.Nm
to listen on the specified interface and port.
By default,
.Nm
will listen on all interfaces and select a random port.
.It Ar verbose
Extra logging.
.El
.El
.Ss PEER
Each
.Ar PEER
is defined as follows:
.Qq peer public-key <file-path> [endpoint <ip>[:<port>]] [preshared-key <file-path>] [outfile <file-path>] [wireguard <dev> <peer> <extra_params>]
.Pp
Providing a
.Ar PEER
instructs
.Nm
to exchange keys with the given peer and write the resulting PSK into the given
output file.
You must either specify the outfile or wireguard output option.
.Pp
The parameters of
.Ar PEER
are as follows:
.Bl -tag -width Ds
.It Ar endpoint <ip>[:<port>]
Specifies the address where the peer can be reached.
This will be automatically updated after the first successful key exchange with
the peer.
If this is unspecified, the peer must initiate the connection.
.It Ar preshared-key <file-path>
You may specify a pre-shared key which will be mixed into the final secret.
.It Ar outfile <file-path>
You may specify a file to write the exchanged keys to.
If this option is specified,
.Nm
will write a notification to standard out every time the key is updated.
.It Ar wireguard <dev> <peer> <extra_params>
This allows you to directly specify a wireguard peer to deploy the
pre-shared-key to.
You may specify extra parameters you would pass to
.Qq wg set
besides the preshared-key parameter which is used by
.Nm .
This makes it possible to add peers entirely from
.Nm .
.El
.Sh EXIT STATUS
.Ex -std
.Sh SEE ALSO
.Xr rp 1 ,
.Xr wg 1
.Rs
.%A Karolin Varner
.%A Benjamin Lipp
.%A Wanja Zaeske
.%A Lisa Schmidt
.%D 2023
.%T Rosenpass
.%U https://rosenpass.eu/whitepaper.pdf
.Re
.Sh STANDARDS
This tool is the reference implementation of the Rosenpass protocol, as
specified within the whitepaper referenced above.
.Sh AUTHORS
Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske,
Marei Peischl, Stephan Ajuvo, and Lisa Schmidt.
.Pp
This manual page was written by
.An Emil Engler
.Sh BUGS
The bugs are tracked at
.Lk https://github.com/rosenpass/rosenpass/issues .

119
doc/rp.1
View File

@@ -1,119 +0,0 @@
.Dd $Mdocdate$
.Dt RP 1
.Os
.Sh NAME
.Nm rp
.Nd high-level interface to rosenpass
.Sh SYNOPSIS
.Nm
.Op Ar explain
.Op Ar verbose
.Ar genkey Ar ... | Ar pubkey ... | Ar exchange ...
.Nm
.Op ...
.Ar genkey PRIVATE_KEYS_DIR
.Nm
.Op ...
.Ar pubkey Ar PRIVATE_KEYS_DIR Ar PUBLIC_KEYS_DIR
.Nm
.Op ...
.\" Splitting this across several lines
.Ar exchange Ar PRIVATE_KEYS_DIR
.Op dev <device>
.Op listen <ip>:<port>
.\" Because the peer argument is complicated, it would be heel to represent it
.\" in mdoc... Using an ugly hack instead, thereby losing semantic.
[peer PUBLIC_KEYS_DIR [endpoint <ip>:<port>] [persistent-keepalive <interval>]
[allowed-ips <ip1>/<cidr1>[,<ip2>/<cidr2>] ...]] ...
.Sh DESCRIPTION
The
.Nm
program
is used to build a VPN with WireGuard and Rosenpass.
.Pp
The optional
.Op explain
and
.Op verbose
options can be used to obtain further help or to enable a detailed view on the
operations, respectively.
.Ss COMMANDS
.Bl -tag -width Ds
.It Ar genkey Ar PRIVATE_KEYS_DIR
Creates a new directory with appropriate permissions and generates all the
necessary private keys required for a peer to participate in a rosenpass
connection.
.It Ar pubkey Ar PRIVATE_KEYS_DIR Ar PUBLIC_KEYS_DIR
Creates a fresh directory at
.Ar PUBLIC_KEYS_DIR ,
which contains the extracted public keys from the private keys generated by
.Ar genkey
and located inside
.Ar PRIVATE_KEYS_DIR .
.It Ar exchange Ar PRIVATE_KEYS_DIR [dev <device>] [listen <ip>:<port>] [PEERS]
Starts the VPN on interface
.Ar device ,
listening on the provided IP and port combination, allowing connections from
.Ar PEERS .
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
In this example, we will assume that the server has an interface bound to
192.168.0.1, that accepts incoming connections on port 9999/UDP for Rosenpass
and port 10000/UDP for WireGuard.
.Pp
To create a VPN connection, start by generating secret keys on both hosts.
.Bd -literal -offset indent
rp genkey server.rosenpass-secret
rp genkey client.rosenpass-secret
.Ed
.Pp
Extract the public keys:
.Bd -literal -offset indent
rp pubkey server.rosenpass-secret server.rosenpass-public
rp pubkey client.rosenpass-secret client.rosenpass-public
.Ed
.Pp
Copy the
.Qq -public
directories to the other peers and then start the VPN.
On the server:
.Bd -literal -offset indent
sudo rp exchange server.rosenpass-secret dev rosenpass0 listen 192.168.0.1:9999 \\
peer client.rosenpass-public allowed-ips fe80::/64
.Ed
.Pp
On the client:
.Bd -literal -offset indent
sudo rp exchange client.rosenpass-secret dev rosenpass 0 \\
peer server.rosenpass-public endpoint 192.168.0.1:9999 allowed-ips fe80::/64
.Ed
.Pp
Assign IP addresses:
.Bd -literal -offset indent
sudo ip a add fe80::1/64 dev rosenpass0 # Server
sudo ip a add fe80::2/64 dev rosenpass0 # Client
.Ed
.Pp
Test the connection by pinging the server on the client machine:
.Bd -literal -offset indent
ping fe80::1%rosenpass0 # Client
.Ed
.Pp
You can watch how rosenpass replaces the WireGuard PSK with the following:
.Bd -literal -offset indent
watch -n 0.2 'wg show all; wg show all preshared-keys'
.Ed
.Sh SEE ALSO
.Xr rosenpass 1 ,
.Xr wg 1
.Sh AUTHORS
Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske,
Marei Peischl, Stephan Ajuvo, and Lisa Schmidt.
.Pp
This manual page was written by
.An Emil Engler
.Sh BUGS
The bugs are tracked at
.Lk https://github.com/rosenpass/rosenpass/issues .

81
flake.lock generated
View File

@@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1699770036,
"narHash": "sha256-bZmI7ytPAYLpyFNgj5xirDkKuAniOkj1xHdv5aIJ5GM=",
"lastModified": 1674240251,
"narHash": "sha256-AVMmf/CtcGensTZmMicToDpOwySEGNKYgRPC7lu3m8w=",
"owner": "nix-community",
"repo": "fenix",
"rev": "81ab0b4f7ae9ebb57daa0edf119c4891806e4d3a",
"rev": "d8067f4d1d3d30732703209bec5ca7d62aaececc",
"type": "github"
},
"original": {
@@ -22,15 +22,12 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
@@ -39,33 +36,13 @@
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1698846319,
"narHash": "sha256-4jyW/dqFBVpWFnhl0nvP6EN4lP7/ZqPxYRjl6var0Oc=",
"lastModified": 1672968032,
"narHash": "sha256-26Jns3GmHem44a06UN5Rj/KOD9qNJThyQrom02Ijur8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "34bdaaf1f0b7fb6d9091472edc968ff10a8c2857",
"rev": "2dea8991d89b9f1e78d874945f78ca15f6954289",
"type": "github"
},
"original": {
@@ -73,22 +50,37 @@
"type": "indirect"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1676496762,
"narHash": "sha256-GFAxjaTgh8KJ8q7BYaI4EVGI5K98ooW70fG/83rSb08=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1bddde315297c092712b0ef03d9def7a474b28ae",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1699715108,
"narHash": "sha256-yPozsobJU55gj+szgo4Lpcg1lHvGQYAT6Y4MrC80mWE=",
"lastModified": 1674162026,
"narHash": "sha256-iY0bxoVE7zAZmp0BB/m5hZW5pWHUfgntDvc1m2zyt/U=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "5fcf5289e726785d20d3aa4d13d90a43ed248e83",
"rev": "6e52c64031825920983515b9e975e93232739f7f",
"type": "github"
},
"original": {
@@ -97,21 +89,6 @@
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

216
flake.nix
View File

@@ -1,11 +1,8 @@
{
inputs = {
nixpkgs-unstable.url = "github:NixOS/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
# for quicker rust builds
naersk.url = "github:nix-community/naersk";
naersk.inputs.nixpkgs.follows = "nixpkgs";
# for rust nightly with llvm-tools-preview
fenix.url = "github:nix-community/fenix";
fenix.inputs.nixpkgs.follows = "nixpkgs";
@@ -22,16 +19,12 @@
"aarch64-linux"
# unsuported best-effort
"i686-linux"
"x86_64-darwin"
"aarch64-darwin"
# "x86_64-windows"
]
(system:
let
scoped = (scope: scope.result);
lib = nixpkgs.lib;
# normal nixpkgs
pkgs = import nixpkgs {
inherit system;
@@ -54,41 +47,14 @@
)
];
};
# parsed Cargo.toml
cargoToml = builtins.fromTOML (builtins.readFile ./rosenpass/Cargo.toml);
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
# source files relevant for rust
src = scoped rec {
# File suffices to include
extensions = [
"lock"
"rs"
"toml"
];
# Files to explicitly include
files = [
"to/README.md"
];
src = ./.;
filter = (path: type: scoped rec {
inherit (lib) any id removePrefix hasSuffix;
anyof = (any id);
basename = baseNameOf (toString path);
relative = removePrefix (toString src + "/") (toString path);
result = anyof [
(type == "directory")
(any (ext: hasSuffix ".${ext}" basename) extensions)
(any (file: file == relative) files)
];
});
result = pkgs.lib.sources.cleanSourceWith { inherit src filter; };
};
src = pkgs.lib.sourceByRegex ./. [
"Cargo\\.(toml|lock)"
"(src|benches)(/.*\\.(rs|md))?"
"rp"
];
# builds a bin path for all dependencies for the `rp` shellscript
rpBinPath = p: with p; lib.makeBinPath [
coreutils
@@ -96,119 +62,60 @@
gawk
wireguard-tools
];
# a function to generate a nix derivation for rosenpass 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;
isStatic = p.stdenv.hostPlatform.isStatic;
in
p.rustPlatform.buildRustPackage {
# metadata and source
pname = cargoToml.package.name;
version = cargoToml.package.version;
inherit src;
cargoLock = {
lockFile = src + "/Cargo.lock";
};
# 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;
nativeBuildInputs = with pkgs; [
cmake # for oqs build in the oqs-sys crate
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 libsodium ];
cargoBuildOptions = x: x ++ [ "-p" "rosenpass" ];
cargoTestOptions = x: x ++ [ "-p" "rosenpass" ];
doCheck = true;
nativeBuildInputs = with pkgs; [
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 libsodium ];
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...
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";
});
# nix defaults to building for aarch64 _without_ the armv8-a
# crypto extensions, but liboqs depens on these
preBuild =
if system == "aarch64-linux" then ''
NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -march=armv8-a+crypto"
'' else "";
preInstall = ''
install -D rp $out/bin/rp
wrapProgram $out/bin/rp --prefix PATH : "${ rpBinPath p }"
'';
# nix progated the *.dev outputs of buildInputs for static
# builds, but that is non-sense for an executables only package
postFixup =
if isStatic then ''
remove-references-to -t ${p.bash.dev} -t ${p.libsodium.dev} \
$out/nix-support/propagated-build-inputs
'' else "";
meta = with pkgs.lib; {
inherit (cargoToml.package) description homepage;
license = with licenses; [ mit asl20 ];
maintainers = [ maintainers.wucke13 ];
platforms = platforms.all;
};
};
# a function to generate a docker image based of rosenpass
rosenpassOCI = name: pkgs.dockerTools.buildImage rec {
inherit name;
@@ -271,11 +178,14 @@
#
packages.whitepaper =
let
pkgs = import inputs.nixpkgs-unstable {
inherit system;
};
tlsetup = (pkgs.texlive.combine {
inherit (pkgs.texlive) scheme-basic acmart amsfonts ccicons
csquotes csvsimple doclicense fancyvrb fontspec gobble
koma-script ifmtarg latexmk lm markdown mathtools minted noto
nunito pgf soul unicode-math lualatex-math paralist
nunito pgf soul soulutf8 unicode-math lualatex-math
gitinfo2 eso-pic biblatex biblatex-trad biblatex-software
xkeyval xurl xifthen biber;
});
@@ -291,6 +201,7 @@
];
buildPhase = ''
export HOME=$(mktemp -d)
export OSFONTDIR="$(kpsewhich --var-value TEXMF)/fonts/{opentype/public/nunito,truetype/google/noto}"
latexmk -r tex/CI.rc
'';
installPhase = ''
@@ -311,7 +222,7 @@
packages.proof-proverif = pkgs.stdenv.mkDerivation {
name = "rosenpass-proverif-proof";
version = "unstable";
src = pkgs.lib.sources.sourceByRegex ./. [
src = pkgs.lib.sourceByRegex ./. [
"analyze.sh"
"marzipan(/marzipan.awk)?"
"analysis(/.*)?"
@@ -332,7 +243,6 @@
inherit (packages.proof-proverif) CRYPTOVERIF_LIB;
inputsFrom = [ packages.default ];
nativeBuildInputs = with pkgs; [
cmake # override the fakecmake from the main step above
cargo-release
clippy
nodePackages.prettier
@@ -347,10 +257,12 @@
checks = {
cargo-fmt = pkgs.runCommand "check-cargo-fmt"
{ inherit (self.devShells.${system}.default) nativeBuildInputs buildInputs; } ''
cargo fmt --manifest-path=${./.}/Cargo.toml --check --all && touch $out
'';
# Blocked by https://github.com/rust-lang/rustfmt/issues/4306
# @dakoraa wants a coding style suitable for her accessible coding setup
# cargo-fmt = pkgs.runCommand "check-cargo-fmt"
# { inherit (devShells.default) nativeBuildInputs buildInputs; } ''
# cargo fmt --manifest-path=${src}/Cargo.toml --check > $out
# '';
nixpkgs-fmt = pkgs.runCommand "check-nixpkgs-fmt"
{ nativeBuildInputs = [ pkgs.nixpkgs-fmt ]; } ''
nixpkgs-fmt --check ${./.} && touch $out
@@ -360,8 +272,6 @@
cd ${./.} && prettier --check . && touch $out
'';
};
formatter = pkgs.nixpkgs-fmt;
}))
];
}

View File

@@ -1,115 +0,0 @@
#!/usr/bin/env bash
# Parse command line options
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
mode="$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Check if mode is specified
if [ -z "$mode" ]; then
echo "Please specify the mode using --mode option. Valid modes are 'check' and 'fix'."
exit 1
fi
# Find all Markdown files in the current directory and its subdirectories
mapfile -t md_files < <(find . -type f -name "*.md")
count=0
# Iterate through each Markdown file
for file in "${md_files[@]}"; do
# Use awk to extract Rust code blocks enclosed within triple backticks
rust_code_blocks=$(awk '/```rust/{flag=1; next}/```/{flag=0} flag' "$file")
# Count the number of Rust code blocks
num_fences=$(awk '/```rust/{f=1} f{if(/```/){f=0; count++}} END{print count}' "$file")
if [ -n "$rust_code_blocks" ]; then
echo "Processing Rust code in $file"
# Iterate through each Rust code block
for ((i=1; i <= num_fences ; i++)); do
# Extract individual Rust code block using awk
current_rust_block=$(awk -v i="$i" '/```rust/{f=1; if (++count == i) next} f&&/```/{f=0;next} f' "$file")
# Variable to check if we have added the main function
add_main=0
# Check if the Rust code block is already inside a function
if ! echo "$current_rust_block" | grep -q "fn main()"; then
# If not, wrap it in a main function
current_rust_block=$'fn main() {\n'"$current_rust_block"$'\n}'
add_main=1
fi
if [ "$mode" == "check" ]; then
# Apply changes to the Rust code block
formatted_rust_code=$(echo "$current_rust_block" | rustfmt)
# Use rustfmt to format the Rust code block, remove first and last lines, and remove the first 4 spaces if added main function
if [ "$add_main" == 1 ]; then
formatted_rust_code=$(echo "$formatted_rust_code" | sed '1d;$d' | sed 's/^ //')
current_rust_block=$(echo "$current_rust_block" | sed '1d;')
current_rust_block=$(echo "$current_rust_block" | sed '$d')
fi
if [ "$formatted_rust_code" == "$current_rust_block" ]; then
echo "No changes needed in Rust code block $i in $file"
else
echo -e "\nChanges needed in Rust code block $i in $file:\n"
echo "$formatted_rust_code"
count=+1
fi
elif [ "$mode" == "fix" ]; then
# Replace current_rust_block with formatted_rust_code in the file
formatted_rust_code=$(echo "$current_rust_block" | rustfmt)
# Use rustfmt to format the Rust code block, remove first and last lines, and remove the first 4 spaces if added main function
if [ "$add_main" == 1 ]; then
formatted_rust_code=$(echo "$formatted_rust_code" | sed '1d;$d' | sed 's/^ //')
current_rust_block=$(echo "$current_rust_block" | sed '1d;')
current_rust_block=$(echo "$current_rust_block" | sed '$d')
fi
# Check if the formatted code is the same as the current Rust code block
if [ "$formatted_rust_code" == "$current_rust_block" ]; then
echo "No changes needed in Rust code block $i in $file"
else
echo "Formatting Rust code block $i in $file"
# Replace current_rust_block with formatted_rust_code in the file
# Use awk to find the line number of the pattern
start_line=$(grep -n "^\`\`\`rust" "$file" | sed -n "${i}p" | cut -d: -f1)
end_line=$(grep -n "^\`\`\`" "$file" | awk -F: -v start_line="$start_line" '$1 > start_line {print $1; exit;}')
if [ -n "$start_line" ] && [ -n "$end_line" ]; then
# Print lines before the Rust code block
head -n "$((start_line - 1))" "$file"
# Print the formatted Rust code block
echo "\`\`\`rust"
echo "$formatted_rust_code"
echo "\`\`\`"
# Print lines after the Rust code block
tail -n +"$((end_line + 1))" "$file"
else
# Rust code block not found or end line not found
cat "$file"
fi > tmpfile && mv tmpfile "$file"
fi
else
echo "Unknown mode: $mode. Valid modes are 'check' and 'fix'."
exit 1
fi
done
fi
done
# CI failure if changes are needed
if [ $count -gt 0 ]; then
echo "CI failed: Changes needed in Rust code blocks."
exit 1
fi

4
fuzz/.gitignore vendored
View File

@@ -1,4 +0,0 @@
target
corpus
artifacts
coverage

1286
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,60 +0,0 @@
[package]
name = "rosenpass-fuzzing"
version = "0.0.1"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
arbitrary = { workspace = true }
libfuzzer-sys = { workspace = true }
stacker = { workspace = true }
rosenpass-secret-memory = { workspace = true }
rosenpass-ciphers = { workspace = true }
rosenpass-cipher-traits = { workspace = true }
rosenpass-to = { workspace = true }
rosenpass = { workspace = true }
[[bin]]
name = "fuzz_handle_msg"
path = "fuzz_targets/handle_msg.rs"
test = false
doc = false
[[bin]]
name = "fuzz_blake2b"
path = "fuzz_targets/blake2b.rs"
test = false
doc = false
[[bin]]
name = "fuzz_aead_enc_into"
path = "fuzz_targets/aead_enc_into.rs"
test = false
doc = false
[[bin]]
name = "fuzz_mceliece_encaps"
path = "fuzz_targets/mceliece_encaps.rs"
test = false
doc = false
[[bin]]
name = "fuzz_kyber_encaps"
path = "fuzz_targets/kyber_encaps.rs"
test = false
doc = false
[[bin]]
name = "fuzz_box_secret_alloc"
path = "fuzz_targets/box_secret_alloc.rs"
test = false
doc = false
[[bin]]
name = "fuzz_vec_secret_alloc"
path = "fuzz_targets/vec_secret_alloc.rs"
test = false
doc = false

View File

@@ -1,29 +0,0 @@
#![no_main]
extern crate arbitrary;
extern crate rosenpass;
use libfuzzer_sys::fuzz_target;
use rosenpass_ciphers::aead;
#[derive(arbitrary::Arbitrary, Debug)]
pub struct Input {
pub key: [u8; 32],
pub nonce: [u8; 12],
pub ad: Box<[u8]>,
pub plaintext: Box<[u8]>,
}
fuzz_target!(|input: Input| {
let mut ciphertext: Vec<u8> = Vec::with_capacity(input.plaintext.len() + 16);
ciphertext.resize(input.plaintext.len() + 16, 0);
aead::encrypt(
ciphertext.as_mut_slice(),
&input.key,
&input.nonce,
&input.ad,
&input.plaintext,
)
.unwrap();
});

View File

@@ -1,20 +0,0 @@
#![no_main]
extern crate arbitrary;
extern crate rosenpass;
use libfuzzer_sys::fuzz_target;
use rosenpass_ciphers::subtle::blake2b;
use rosenpass_to::To;
#[derive(arbitrary::Arbitrary, Debug)]
pub struct Blake2b {
pub key: [u8; 32],
pub data: Box<[u8]>,
}
fuzz_target!(|input: Blake2b| {
let mut out = [0u8; 32];
blake2b::hash(&input.key, &input.data).to(&mut out).unwrap();
});

View File

@@ -1,8 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use rosenpass_secret_memory::alloc::secret_box;
fuzz_target!(|data: &[u8]| {
let _ = secret_box(data);
});

View File

@@ -1,18 +0,0 @@
#![no_main]
extern crate rosenpass;
use libfuzzer_sys::fuzz_target;
use rosenpass::protocol::CryptoServer;
use rosenpass_secret_memory::Secret;
fuzz_target!(|rx_buf: &[u8]| {
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];
// We expect errors while fuzzing therefore we do not check the result.
let _ = cs.handle_msg(rx_buf, &mut tx_buf);
});

View File

@@ -1,20 +0,0 @@
#![no_main]
extern crate arbitrary;
extern crate rosenpass;
use libfuzzer_sys::fuzz_target;
use rosenpass_cipher_traits::Kem;
use rosenpass_ciphers::kem::EphemeralKem;
#[derive(arbitrary::Arbitrary, Debug)]
pub struct Input {
pub pk: [u8; 800],
}
fuzz_target!(|input: Input| {
let mut ciphertext = [0u8; 768];
let mut shared_secret = [0u8; 32];
EphemeralKem::encaps(&mut shared_secret, &mut ciphertext, &input.pk).unwrap();
});

View File

@@ -1,15 +0,0 @@
#![no_main]
extern crate rosenpass;
use libfuzzer_sys::fuzz_target;
use rosenpass_cipher_traits::Kem;
use rosenpass_ciphers::kem::StaticKem;
fuzz_target!(|input: [u8; StaticKem::PK_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);
});

View File

@@ -1,9 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use rosenpass_secret_memory::alloc::secret_vec;
fuzz_target!(|data: &[u8]| {
let mut vec = secret_vec();
vec.extend_from_slice(data);
});

View File

@@ -1,16 +0,0 @@
[package]
name = "rosenpass-oqs"
authors = ["Karolin Varner <karo@cupdev.net>", "wucke13 <wucke13@gmail.com>"]
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Rosenpass internal bindings to liboqs"
homepage = "https://rosenpass.eu/"
repository = "https://github.com/rosenpass/rosenpass"
readme = "readme.md"
[dependencies]
rosenpass-cipher-traits = { workspace = true }
rosenpass-util = { workspace = true }
oqs-sys = { workspace = true }
paste = { workspace = true }

View File

@@ -1,5 +0,0 @@
# Rosenpass internal liboqs bindings
Rosenpass internal library providing bindings to liboqs.
This is an internal library; not guarantee is made about its API at this point in time.

View File

@@ -1,80 +0,0 @@
macro_rules! oqs_kem {
($name:ident) => { ::paste::paste!{
mod [< $name:snake >] {
use rosenpass_cipher_traits::Kem;
use rosenpass_util::result::Guaranteed;
pub enum [< $name:camel >] {}
/// # Panic & Safety
///
/// This Trait impl calls unsafe [oqs_sys] functions, that write to byte
/// slices only identified using raw pointers. It must be ensured that the raw
/// pointers point into byte slices of sufficient length, to avoid UB through
/// overwriting of arbitrary data. This is ensured through assertions in the
/// implementation.
///
/// __Note__: This requirement is stricter than necessary, it would suffice
/// to only check that the buffers are big enough, allowing them to be even
/// bigger. However, from a correctness point of view it does not make sense to
/// allow bigger buffers.
impl Kem for [< $name:camel >] {
type Error = ::std::convert::Infallible;
const SK_LEN: usize = ::oqs_sys::kem::[<OQS_KEM _ $name:snake _ length_secret_key >] as usize;
const PK_LEN: usize = ::oqs_sys::kem::[<OQS_KEM _ $name:snake _ length_public_key >] as usize;
const CT_LEN: usize = ::oqs_sys::kem::[<OQS_KEM _ $name:snake _ length_ciphertext >] as usize;
const SHK_LEN: usize = ::oqs_sys::kem::[<OQS_KEM _ $name:snake _ length_shared_secret >] as usize;
fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Guaranteed<()> {
assert_eq!(sk.len(), Self::SK_LEN);
assert_eq!(pk.len(), Self::PK_LEN);
unsafe {
oqs_call!(
::oqs_sys::kem::[< OQS_KEM _ $name:snake _ keypair >],
pk.as_mut_ptr(),
sk.as_mut_ptr()
);
}
Ok(())
}
fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Guaranteed<()> {
assert_eq!(shk.len(), Self::SHK_LEN);
assert_eq!(ct.len(), Self::CT_LEN);
assert_eq!(pk.len(), Self::PK_LEN);
unsafe {
oqs_call!(
::oqs_sys::kem::[< OQS_KEM _ $name:snake _ encaps >],
ct.as_mut_ptr(),
shk.as_mut_ptr(),
pk.as_ptr()
);
}
Ok(())
}
fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Guaranteed<()> {
assert_eq!(shk.len(), Self::SHK_LEN);
assert_eq!(sk.len(), Self::SK_LEN);
assert_eq!(ct.len(), Self::CT_LEN);
unsafe {
oqs_call!(
::oqs_sys::kem::[< OQS_KEM _ $name:snake _ decaps >],
shk.as_mut_ptr(),
ct.as_ptr(),
sk.as_ptr()
);
}
Ok(())
}
}
}
pub use [< $name:snake >] :: [< $name:camel >];
}}
}

View File

@@ -1,21 +0,0 @@
macro_rules! oqs_call {
($name:path, $($args:expr),*) => {{
use oqs_sys::common::OQS_STATUS::*;
match $name($($args),*) {
OQS_SUCCESS => {}, // nop
OQS_EXTERNAL_LIB_ERROR_OPENSSL => {
panic!("OpenSSL error in liboqs' {}.", stringify!($name));
},
OQS_ERROR => {
panic!("Unknown error in liboqs' {}.", stringify!($name));
}
}
}};
($name:ident) => { oqs_call!($name, ) };
}
#[macro_use]
mod kem_macro;
oqs_kem!(kyber_512);
oqs_kem!(classic_mceliece_460896);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 725 KiB

View File

@@ -1345,7 +1345,7 @@
<g transform="matrix(1,0,0,1,420.66,-1031.32)">
<g transform="matrix(31.25,0,0,31.25,1431.32,1459.33)">
</g>
<text x="1179.63px" y="1459.33px" style="font-family:'Nunito-Medium', 'Nunito';font-weight:500;font-size:31.25px;">&quot;chaining k<tspan x="1334px 1350.47px " y="1459.33px 1459.33px ">ey</tspan> init&quot;</text>
<text x="1179.63px" y="1459.33px" style="font-family:'Nunito-Medium', 'Nunito';font-weight:500;font-size:31.25px;">&quot;k<tspan x="1207.79px 1224.25px " y="1459.33px 1459.33px ">ey</tspan> chaining init&quot;</text>
</g>
</g>
<g transform="matrix(0.389246,0,0,0.136584,299.374,1166.87)">
@@ -1437,7 +1437,7 @@
<g transform="matrix(0.99675,0,0,0.996238,-597.124,-172.692)">
<g transform="matrix(31.25,0,0,31.25,1492.94,1459.33)">
</g>
<text x="1187.16px" y="1459.33px" style="font-family:'Nunito-Medium', 'Nunito';font-weight:500;font-size:31.25px;">&quot;chaining k<tspan x="1341.54px 1358px " y="1459.33px 1459.33px ">ey</tspan> e<tspan x="1398.88px " y="1459.33px ">x</tspan>tr<tspan x="1437.88px " y="1459.33px ">a</tspan>ct&quot;</text>
<text x="1187.16px" y="1459.33px" style="font-family:'Nunito-Medium', 'Nunito';font-weight:500;font-size:31.25px;">&quot;k<tspan x="1215.32px 1231.79px " y="1459.33px 1459.33px ">ey</tspan> chaining e<tspan x="1398.88px " y="1459.33px ">x</tspan>tr<tspan x="1437.88px " y="1459.33px ">a</tspan>ct&quot;</text>
</g>
<g transform="matrix(0.99675,0,0,0.996238,-380.054,-779.158)">
<g transform="matrix(31.25,0,0,31.25,1463.54,1459.33)">

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

View File

@@ -79,8 +79,6 @@
letter-csv .initial:n = ,
letter-content .tl_set:N = \l_letter_csv_content_tl,
letter-content .initial:n=,
tableofcontents .bool_gset:N = \g__ptxcd_tableofcontents_bool,
tableofcontents .initial:n = true,
}
\tl_new:N \l__markdown_sequence_tl

View File

@@ -171,17 +171,8 @@ version={4.0},
\ExplSyntaxOn
\SetTemplatePreamble{
\hypersetup{pdftitle=\inserttitle,pdfauthor=The~Rosenpass~Project}
\exp_args:NV\tl_if_eq:nnTF \inserttitle{Rosenpass} {
\title{\vspace*{-2.5cm}\includegraphics[width=4cm]{RosenPass-Logo}}
} {
\titlehead{\centerline{\includegraphics[width=4cm]{RosenPass-Logo}}}
\title{\inserttitle}
}
\ifx\csname insertauthor\endcsname\relax
\author{}
\else
\author{\parbox{\linewidth}{\centering\insertauthor}}
\fi
\title{\vspace*{-2.5cm}\includegraphics[width=4cm]{RosenPass-Logo}}
\author{\csname insertauthor\endcsname}
\subject{\csname insertsubject\endcsname}
\date{\vspace{-1cm}}
}
@@ -383,28 +374,29 @@ version={4.0},
}
}
}
\makeatother
\ExplSyntaxOff
% end of namepartpicturesetup
\newcommand{\captionbox}[1]{{\setlength{\fboxsep}{.5ex}\colorbox{rosenpass-gray}{#1}}}
\makeatletter
\renewenvironment{abstract}{
\small
\begin{center}\normalfont\sectfont\nobreak\abstractname\@endparpenalty\@M\end{center}%
}{
\par
}
\makeatother
\SetTemplateBegin{
\maketitle
\begin{abstract}
\noindent\csname insertabstract\endcsname
\end{abstract}
\bool_if:NT \g__ptxcd_tableofcontents_bool \tableofcontents
\tableofcontents
\clearpage
}
\makeatother
\ExplSyntaxOff
\SetTemplateEnd{}
\SetTemplateEnd{
}

View File

@@ -7,13 +7,13 @@ author:
- Wanja Zaeske
- Lisa Schmidt = {Scientific Illustrator \\url{mullana.de}}
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.
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 some other 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.
The WireGuard implementation enjoys great trust from the cryptography community and has excellent performance characteristics. To preserve these features, the Rosenpass application runs side-by-side with WireGuard and supplies a new post-quantum-secure pre-shared key (PSK) every two minutes. WireGuard itself still performs the pre-quantum-secure key exchange and transfers any transport data with no involvement from Rosenpass at all.
The Rosenpass project consists of a protocol description, an implementation written in Rust, and a symbolic analysis of the protocols security using ProVerif [@proverif]. We are working on a cryptographic security proof using CryptoVerif [@cryptoverif].
This document is a guide for engineers and researchers implementing the protocol; a scientific paper discussing the security properties of Rosenpass is work in progress.
This document is a guide to engineers and researchers implementing the protocol; a scientific paper discussing the security properties of Rosenpass is work in progress.
---
\enlargethispage{5mm}
@@ -169,7 +169,7 @@ Rosenpass uses a cryptographic hash function for multiple purposes:
* Computing the cookie to guard against denial of service attacks. This is a feature adopted from WireGuard, but not yet included in the implementation of Rosenpass.
* Computing the peer ID
* Key derivation during and after the handshake
* Computing the additional data for the biscuit encryption, to provide some privacy for its contents
* Computing the additional data for the biscuit encryption, to prove some privacy for its contents
Using one hash function for multiple purposes can cause real-world security issues and even key recovery attacks [@oraclecloning]. We choose a tree-based domain separation scheme based on a keyed hash function the previously introduced primitive `hash` to make sure all our hash function calls can be seen as distinct.
@@ -237,7 +237,7 @@ For each peer, the server stores:
The initiator stores the following local state for each ongoing handshake:
* A reference to the peer structure
* A state indicator to keep track of the next message expected from the responder
* A state indicator to keep track of the message expected from the responder next
* `sidi` Initiator session ID
* `sidr` Responder session ID
* `ck` The chaining key

View File

@@ -14,14 +14,14 @@ This repository contains
## Getting started
First, [install rosenpass](#Getting-Rosenpass). Then, check out the help functions of `rp` & `rosenpass`:
First, [install rosenpass](#Getting-Rosenpass). Then, check out the help funtions of `rp` & `rosenpass`:
```sh
rp help
rosenpass help
```
Follow [quick start instructions](https://rosenpass.eu/#start) to get a VPN up and running.
Follow [quickstart instructions](https://rosenpass.eu/#start) to get a VPN up and running.
## Software architecture
@@ -54,7 +54,7 @@ We are working on a cryptographic proof of security, but we already provide a sy
(manual) $ ./analyze.sh
```
The analysis is implemented according to modern software engineering principles: Using the C preprocessor, we where able to split the analysis into multiple files and uses some meta programming to avoid repetition.
The analysis is implemented according to modern software engineering principles: Using the C preprocessor, we where able to split the analysis into multiple files and uses some metaprogramming to avoid repetition.
The code uses a variety of optimizations to speed up analysis such as using secret functions to model trusted/malicious setup. We split the model into two separate entry points which can be analyzed in parallel. Each is much faster than both models combined.
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.
@@ -62,22 +62,15 @@ A wrapper script provides instant feedback about which queries execute as expect
[^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
[^pqwg-statedis]: Unless supplied with a pre-shared-key, but this defeates the purpose of a key exchange protocol
[^wg-statedis]: https://lists.zx2c4.com/pipermail/wireguard/2021-August/006916.htmlA
# Getting Rosenpass
Rosenpass is packaged for more and more distributions, maybe also for the distribution of your choice?
Rosenpass is packaged for more and more distros, maybe also for the distro of your choice?
[![Packaging status](https://repology.org/badge/vertical-allrepos/rosenpass.svg)](https://repology.org/project/rosenpass/versions)
# Mirrors
Don't want to use GitHub or only have an IPv6 connection? Rosenpass has set up two mirrors for this:
- [NotABug](https://notabug.org/rosenpass/rosenpass)
- [GitLab](https://gitlab.com/rosenpass/rosenpass/)
# Supported by
Funded through <a href="https://nlnet.nl/">NLNet</a> with financial support for the European Commission's <a href="https://nlnet.nl/assure">NGI Assure</a> program.

View File

@@ -1,44 +1,35 @@
[package]
name = "rosenpass"
version = "0.2.1"
version = "0.1.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"
readme = "../readme.md"
[[bench]]
name = "handshake"
harness = false
[dependencies]
rosenpass-util = { workspace = true }
rosenpass-constant-time = { workspace = true }
rosenpass-ciphers = { workspace = true }
rosenpass-cipher-traits = { workspace = true }
rosenpass-to = { workspace = true }
rosenpass-secret-memory = { workspace = true }
anyhow = { workspace = true }
static_assertions = { workspace = true }
memoffset = { workspace = true }
thiserror = { workspace = true }
paste = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }
clap = { workspace = true }
mio = { workspace = true }
rand = { workspace = true }
zerocopy = { workspace = true }
home = { workspace = true }
[build-dependencies]
anyhow = { workspace = true }
anyhow = { version = "1.0.52", features = ["backtrace"] }
base64 = "0.13.0"
clap = { version = "3.0.0", features = ["yaml"] }
static_assertions = "1.1.0"
memoffset = "0.6.5"
libsodium-sys-stable = { version = "1.19.26", features = ["use-pkg-config"] }
oqs-sys = { version = "0.7.1", default-features = false, features = ['classic_mceliece', 'kyber'] }
lazy_static = "1.4.0"
thiserror = "1.0.38"
paste = "1.0.11"
log = { version = "0.4.17", optional = true }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
criterion = { workspace = true }
test_bin = { workspace = true }
stacker = { workspace = true }
criterion = "0.3.5"
test_bin = "0.4.0"
[features]
default = ["log", "env_logger"]

View File

@@ -1,16 +1,17 @@
use anyhow::Result;
use rosenpass::protocol::{CryptoServer, HandleMsgResult, MsgBuf, PeerPtr, SPk, SSk, SymKey};
use rosenpass_cipher_traits::Kem;
use rosenpass_ciphers::kem::StaticKem;
use rosenpass::{
pqkem::{CCAKEM, KEM},
protocol::{CcaPk, CcaSk, HandleMsgResult, MsgBuf, PeerPtr, Server, SymKey},
sodium::sodium_init,
};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn handle(
tx: &mut CryptoServer,
tx: &mut Server,
msgb: &mut MsgBuf,
msgl: usize,
rx: &mut CryptoServer,
rx: &mut Server,
resb: &mut MsgBuf,
) -> Result<(Option<SymKey>, Option<SymKey>)> {
let HandleMsgResult {
@@ -29,7 +30,7 @@ fn handle(
Ok((txk, rxk.or(xch)))
}
fn hs(ini: &mut CryptoServer, res: &mut CryptoServer) -> Result<()> {
fn hs(ini: &mut Server, res: &mut Server) -> Result<()> {
let (mut inib, mut resb) = (MsgBuf::zero(), MsgBuf::zero());
let sz = ini.initiate_handshake(PeerPtr(0), &mut *inib)?;
let (kini, kres) = handle(ini, &mut inib, sz, res, &mut resb)?;
@@ -37,34 +38,32 @@ fn hs(ini: &mut CryptoServer, res: &mut CryptoServer) -> Result<()> {
Ok(())
}
fn keygen() -> Result<(SSk, SPk)> {
let (mut sk, mut pk) = (SSk::zero(), SPk::zero());
StaticKem::keygen(sk.secret_mut(), pk.secret_mut())?;
fn keygen() -> Result<(CcaSk, CcaPk)> {
let (mut sk, mut pk) = (CcaSk::zero(), CcaPk::zero());
CCAKEM::keygen(sk.secret_mut(), pk.secret_mut())?;
Ok((sk, pk))
}
fn make_server_pair() -> Result<(CryptoServer, CryptoServer)> {
fn make_server_pair() -> Result<(Server, Server)> {
let psk = SymKey::random();
let ((ska, pka), (skb, pkb)) = (keygen()?, keygen()?);
let (mut a, mut b) = (
CryptoServer::new(ska, pka.clone()),
CryptoServer::new(skb, pkb.clone()),
);
let (mut a, mut b) = (Server::new(ska, pka.clone()), Server::new(skb, pkb.clone()));
a.add_peer(Some(psk.clone()), pkb)?;
b.add_peer(Some(psk), pka)?;
Ok((a, b))
}
fn criterion_benchmark(c: &mut Criterion) {
sodium_init().unwrap();
let (mut a, mut b) = make_server_pair().unwrap();
c.bench_function("cca_secret_alloc", |bench| {
bench.iter(|| {
SSk::zero();
CcaSk::zero();
})
});
c.bench_function("cca_public_alloc", |bench| {
bench.iter(|| {
SPk::zero();
CcaPk::zero();
})
});
c.bench_function("keygen", |bench| {

View File

@@ -1,52 +0,0 @@
use anyhow::bail;
use anyhow::Result;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
/// Invokes a troff compiler to compile a manual page
fn render_man(compiler: &str, man: &str) -> Result<String> {
let out = Command::new(compiler).args(["-Tascii", man]).output()?;
if !out.status.success() {
bail!("{} returned an error", compiler);
}
Ok(String::from_utf8(out.stdout)?)
}
/// Generates the manual page
fn generate_man() -> String {
// This function is purposely stupid and redundant
let man = render_man("mandoc", "./doc/rosenpass.1");
if let Ok(man) = man {
return man;
}
let man = render_man("groff", "./doc/rosenpass.1");
if let Ok(man) = man {
return man;
}
"Cannot render manual page. Please visit https://rosenpass.eu/docs/manuals/\n".into()
}
fn man() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let man = generate_man();
let path = out_dir.join("rosenpass.1.ascii");
let mut file = File::create(&path).unwrap();
file.write_all(man.as_bytes()).unwrap();
println!("cargo:rustc-env=ROSENPASS_MAN={}", path.display());
}
fn main() {
// For now, rerun the build script on every time, as the build script
// is not very expensive right now.
println!("cargo:rerun-if-changed=./");
man();
}

View File

@@ -1 +0,0 @@
../readme.md

View File

@@ -1,739 +0,0 @@
use anyhow::bail;
use anyhow::Result;
use log::{debug, error, info, warn};
use mio::Interest;
use mio::Token;
use rosenpass_util::file::fopen_w;
use std::cell::Cell;
use std::io::Write;
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::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use std::slice;
use std::thread;
use std::time::Duration;
use crate::{
config::Verbosity,
protocol::{CryptoServer, MsgBuf, PeerPtr, SPk, SSk, SymKey, Timing},
};
use rosenpass_util::attempt;
use rosenpass_util::b64::{b64_writer, fmt_b64};
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);
fn ipv4_any_binding() -> SocketAddr {
// addr, port
SocketAddr::V4(SocketAddrV4::new(IPV4_ANY_ADDR, 0))
}
fn ipv6_any_binding() -> SocketAddr {
// addr, port, flowinfo, scope_id
SocketAddr::V6(SocketAddrV6::new(IPV6_ANY_ADDR, 0, 0, 0))
}
#[derive(Default, Debug)]
pub struct AppPeer {
pub outfile: Option<PathBuf>,
pub outwg: Option<WireguardOut>, // TODO make this a generic command
pub initial_endpoint: Option<Endpoint>,
pub current_endpoint: Option<Endpoint>,
}
impl AppPeer {
pub fn endpoint(&self) -> Option<&Endpoint> {
self.current_endpoint
.as_ref()
.or(self.initial_endpoint.as_ref())
}
}
#[derive(Default, Debug)]
pub struct WireguardOut {
// impl KeyOutput
pub dev: String,
pub pk: String,
pub extra_params: Vec<String>,
}
/// Holds the state of the application, namely the external IO
///
/// Responsible for file IO, network IO
// TODO add user control via unix domain socket and stdin/stdout
#[derive(Debug)]
pub struct AppServer {
pub crypt: CryptoServer,
pub sockets: Vec<mio::net::UdpSocket>,
pub events: mio::Events,
pub mio_poll: mio::Poll,
pub peers: Vec<AppPeer>,
pub verbosity: Verbosity,
pub all_sockets_drained: bool,
}
/// A socket pointer is an index assigned to a socket;
/// right now the index is just the sockets index in AppServer::sockets.
///
/// Holding this as a reference instead of an &mut UdpSocket is useful
/// to deal with the borrow checker, because otherwise we could not refer
/// to a socket and another member of AppServer at the same time.
#[derive(Debug)]
pub struct SocketPtr(pub usize);
impl SocketPtr {
pub fn get<'a>(&self, srv: &'a AppServer) -> &'a mio::net::UdpSocket {
&srv.sockets[self.0]
}
pub fn get_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut mio::net::UdpSocket {
&mut srv.sockets[self.0]
}
pub fn send_to(&self, srv: &AppServer, buf: &[u8], addr: SocketAddr) -> anyhow::Result<()> {
self.get(srv).send_to(buf, addr)?;
Ok(())
}
}
/// Index based pointer to a Peer
#[derive(Debug, Copy, Clone)]
pub struct AppPeerPtr(pub usize);
impl AppPeerPtr {
/// Takes an index based handle and returns the actual peer
pub fn lift(p: PeerPtr) -> Self {
Self(p.0)
}
/// Returns an index based handle to one Peer
pub fn lower(&self) -> PeerPtr {
PeerPtr(self.0)
}
pub fn get_app<'a>(&self, srv: &'a AppServer) -> &'a AppPeer {
&srv.peers[self.0]
}
pub fn get_app_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut AppPeer {
&mut srv.peers[self.0]
}
}
#[derive(Debug)]
pub enum AppPollResult {
DeleteKey(AppPeerPtr),
SendInitiation(AppPeerPtr),
SendRetransmission(AppPeerPtr),
ReceivedMessage(usize, Endpoint),
}
#[derive(Debug)]
pub enum KeyOutputReason {
Exchanged,
Stale,
}
/// Represents a communication partner rosenpass may be sending packets to
///
/// Generally at the start of Rosenpass either no address or a Hostname is known;
/// later when we actually start to receive RespHello packages, we know the specific Address
/// and socket to use with a peer
#[derive(Debug)]
pub enum Endpoint {
/// Rosenpass supports multiple sockets, so we include the information
/// which socket an address can be reached on. This probably does not
/// make much of a difference in most setups where two sockets are just
/// used to enable dual stack operation; it does make a difference in
/// more complex use cases.
///
/// For instance it enables using multiple interfaces with overlapping
/// ip spaces, such as listening on a private IP network and a public IP
/// 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 {
/// 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
// at first can not be resolved but becomes resolvable later.
Discovery(HostPathDiscoveryEndpoint),
}
impl Endpoint {
/// Start discovery from some addresses
pub fn discovery_from_addresses(addresses: Vec<SocketAddr>) -> Self {
Endpoint::Discovery(HostPathDiscoveryEndpoint::from_addresses(addresses))
}
/// Start endpoint discovery from a hostname
pub fn discovery_from_hostname(hostname: String) -> anyhow::Result<Self> {
let host = HostPathDiscoveryEndpoint::lookup(hostname)?;
Ok(Endpoint::Discovery(host))
}
// Restart discovery; joining two sources of (potential) addresses
//
// This is used when the connection to an endpoint is lost in order
// to include the addresses specified on the command line and the
// address last used in the discovery process
pub fn discovery_from_multiple_sources(
a: Option<&Endpoint>,
b: Option<&Endpoint>,
) -> Option<Self> {
let sources = match (a, b) {
(Some(e), None) | (None, Some(e)) => e.addresses().iter().chain(&[]),
(Some(e1), Some(e2)) => e1.addresses().iter().chain(e2.addresses()),
(None, None) => return None,
};
let lower_size_bound = sources.size_hint().0;
let mut dedup = std::collections::HashSet::with_capacity(lower_size_bound);
let mut addrs = Vec::with_capacity(lower_size_bound);
for a in sources {
if dedup.insert(a) {
addrs.push(*a);
}
}
Some(Self::discovery_from_addresses(addrs))
}
pub fn send(&self, srv: &AppServer, buf: &[u8]) -> anyhow::Result<()> {
use Endpoint::*;
match self {
SocketBoundAddress { socket, addr } => socket.send_to(srv, buf, *addr),
Discovery(host) => host.send_scouting(srv, buf),
}
}
fn addresses(&self) -> &[SocketAddr] {
use Endpoint::*;
match self {
SocketBoundAddress { addr, .. } => slice::from_ref(addr),
Discovery(host) => host.addresses(),
}
}
}
/// Handles host-path discovery
///
/// When rosenpass is started, we either know no peer address
/// or we know a hostname. How to contact this hostname may not
/// be entirely clear for two reasons:
///
/// 1. We have multiple sockets; only a subset of those may be able to contact the host
/// 2. DNS resolution can return multiple addresses
///
/// We could just use the first working socket and the first address returned, but this
/// may be error prone: Some of the sockets may appear to be able to contact the host,
/// but the packets will be dropped. Some of the addresses may appear to be reachable
/// but the packets could be lost.
///
/// In contrast to TCP, UDP has no mechanism to ensure packets actually arrive.
///
/// To robustly handle host path discovery, we try each socket-ip-combination in a round
/// robin fashion; the struct stores the offset of the last used combination internally and
/// and will continue with the next combination on every call.
///
/// Retransmission handling will continue normally; i.e. increasing the distance between
/// retransmissions on every retransmission, until it is long enough to bore a human. Therefor
/// it is important to avoid having a large number of sockets drop packets not just for efficiency
/// but to avoid latency issues too.
///
// TODO: We might consider adjusting the retransmission handling to account for host-path discovery
#[derive(Debug)]
pub struct HostPathDiscoveryEndpoint {
scouting_state: Cell<(usize, usize)>, // addr_off, sock_off
addresses: Vec<SocketAddr>,
}
impl HostPathDiscoveryEndpoint {
pub fn from_addresses(addresses: Vec<SocketAddr>) -> Self {
let scouting_state = Cell::new((0, 0));
Self {
addresses,
scouting_state,
}
}
/// Lookup a hostname
pub fn lookup(hostname: String) -> anyhow::Result<Self> {
Ok(Self {
addresses: ToSocketAddrs::to_socket_addrs(&hostname)?.collect(),
scouting_state: Cell::new((0, 0)),
})
}
pub fn addresses(&self) -> &Vec<SocketAddr> {
&self.addresses
}
fn insert_next_scout_offset(&self, srv: &AppServer, addr_no: usize, sock_no: usize) {
self.scouting_state.set((
(addr_no + 1) % self.addresses.len(),
(sock_no + 1) % srv.sockets.len(),
));
}
/// Attempt to reach the host
///
/// Will round-robin-try different socket-ip-combinations on each call.
pub fn send_scouting(&self, srv: &AppServer, buf: &[u8]) -> anyhow::Result<()> {
let (addr_off, sock_off) = self.scouting_state.get();
let mut addrs = (self.addresses)
.iter()
.enumerate()
.cycle()
.skip(addr_off)
.take(self.addresses.len());
let mut sockets = (srv.sockets)
.iter()
.enumerate()
.cycle()
.skip(sock_off)
.take(srv.sockets.len());
for (addr_no, addr) in addrs.by_ref() {
for (sock_no, sock) in sockets.by_ref() {
let res = sock.send_to(buf, *addr);
let err = match res {
Ok(_) => {
self.insert_next_scout_offset(srv, addr_no, sock_no);
return Ok(());
}
Err(e) => e,
};
// TODO: replace this by
// e.kind() == io::ErrorKind::NetworkUnreachable
// once https://github.com/rust-lang/rust/issues/86442 lands
let ignore = err
.to_string()
.starts_with("Address family not supported by protocol");
if !ignore {
warn!("Socket #{} refusing to send to {}: ", sock_no, addr);
}
}
}
bail!("Unable to send message: All sockets returned errors.")
}
}
impl AppServer {
pub fn new(
sk: SSk,
pk: SPk,
addrs: Vec<SocketAddr>,
verbosity: Verbosity,
) -> anyhow::Result<Self> {
// setup mio
let mio_poll = mio::Poll::new()?;
let events = mio::Events::with_capacity(8);
// bind each SocketAddr to a socket
let maybe_sockets: Result<Vec<_>, _> =
addrs.into_iter().map(mio::net::UdpSocket::bind).collect();
let mut sockets = maybe_sockets?;
// When no socket is specified, rosenpass should open one port on all
// available interfaces best-effort. Here are the cases how this can possibly go:
//
// Some operating systems (such as Linux [^linux] and FreeBSD [^freebsd])
// using IPv6 sockets to handle IPv4 connections; on these systems
// binding to the `[::]:0` address will typically open a dual-stack
// socket. Some other systems such as OpenBSD [^openbsd] do not support this feature.
//
// Dual-stack systems provide a flag to enable or disable this
// behavior the IPV6_V6ONLY flag. OpenBSD supports this flag
// read-only. MIO[^mio] provides a way to read this flag but not
// to write it.
//
// - One dual-stack IPv6 socket, if the operating supports dual-stack sockets and
// correctly reports this
// - One IPv6 socket and one IPv4 socket if the operating does not support dual stack
// sockets or disables them by default assuming this is also correctly reported
// - One IPv6 socket and no IPv4 socket if IPv6 socket is not dual-stack and opening
// the IPv6 socket fails
// - One IPv4 socket and no IPv6 socket if opening the IPv6 socket fails
// - One dual-stack IPv6 socket and a redundant IPv4 socket if dual-stack sockets are
// supported but the operating system does not correctly report this (specifically,
// if the only_v6() call raises an error)
// - Rosenpass exits if no socket could be opened
//
// [^freebsd]: https://man.freebsd.org/cgi/man.cgi?query=ip6&sektion=4&manpath=FreeBSD+6.0-RELEASE
// [^openbsd]: https://man.openbsd.org/ip6.4
// [^linux]: https://man7.org/linux/man-pages/man7/ipv6.7.html
// [^mio]: https://docs.rs/mio/0.8.6/mio/net/struct.UdpSocket.html#method.only_v6
if sockets.is_empty() {
macro_rules! try_register_socket {
($title:expr, $binding:expr) => {{
let r = mio::net::UdpSocket::bind($binding);
match r {
Ok(sock) => {
sockets.push(sock);
Some(sockets.len() - 1)
}
Err(e) => {
warn!("Could not bind to {} socket: {}", $title, e);
None
}
}
}};
}
let v6 = try_register_socket!("IPv6", ipv6_any_binding());
let need_v4 = match v6.map(|no| sockets[no].only_v6()) {
Some(Ok(v)) => v,
None => true,
Some(Err(e)) => {
warn!("Unable to detect whether the IPv6 socket supports dual-stack operation: {}", e);
true
}
};
if need_v4 {
try_register_socket!("IPv4", ipv4_any_binding());
}
}
if sockets.is_empty() {
bail!("No sockets to listen on!")
}
// register all sockets to mio
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
Ok(Self {
crypt: CryptoServer::new(sk, pk),
peers: Vec::new(),
verbosity,
sockets,
events,
mio_poll,
all_sockets_drained: false,
})
}
pub fn verbose(&self) -> bool {
matches!(self.verbosity, Verbosity::Verbose)
}
pub fn add_peer(
&mut self,
psk: Option<SymKey>,
pk: SPk,
outfile: Option<PathBuf>,
outwg: Option<WireguardOut>,
hostname: Option<String>,
) -> anyhow::Result<AppPeerPtr> {
let PeerPtr(pn) = self.crypt.add_peer(psk, pk)?;
assert!(pn == self.peers.len());
let initial_endpoint = hostname
.map(Endpoint::discovery_from_hostname)
.transpose()?;
let current_endpoint = None;
self.peers.push(AppPeer {
outfile,
outwg,
initial_endpoint,
current_endpoint,
});
Ok(AppPeerPtr(pn))
}
pub fn listen_loop(&mut self) -> anyhow::Result<()> {
const INIT_SLEEP: f64 = 0.01;
const MAX_FAILURES: i32 = 10;
let mut failure_cnt = 0;
loop {
let msgs_processed = 0usize;
let err = match self.event_loop() {
Ok(()) => return Ok(()),
Err(e) => e,
};
// This should not happen…
failure_cnt = if msgs_processed > 0 {
0
} else {
failure_cnt + 1
};
let sleep = INIT_SLEEP * 2.0f64.powf(f64::from(failure_cnt - 1));
let tries_left = MAX_FAILURES - (failure_cnt - 1);
error!(
"unexpected error after processing {} messages: {:?} {}",
msgs_processed,
err,
err.backtrace()
);
if tries_left > 0 {
error!("re-initializing networking in {sleep}! {tries_left} tries left.");
std::thread::sleep(self.crypt.timebase.dur(sleep));
continue;
}
bail!("too many network failures");
}
}
pub fn event_loop(&mut self) -> anyhow::Result<()> {
let (mut rx, mut tx) = (MsgBuf::zero(), MsgBuf::zero());
/// if socket address for peer is known, call closure
/// assumes that closure leaves a message in `tx`
/// assumes that closure returns the length of message in bytes
macro_rules! tx_maybe_with {
($peer:expr, $fn:expr) => {
attempt!({
let p = $peer;
if p.get_app(self).endpoint().is_some() {
let len = $fn()?;
let ep: &Endpoint = p.get_app(self).endpoint().unwrap();
ep.send(self, &tx[..len])?;
}
Ok(())
})
};
}
loop {
use crate::protocol::HandleMsgResult;
use AppPollResult::*;
use KeyOutputReason::*;
match self.poll(&mut *rx)? {
#[allow(clippy::redundant_closure_call)]
SendInitiation(peer) => tx_maybe_with!(peer, || self
.crypt
.initiate_handshake(peer.lower(), &mut *tx))?,
#[allow(clippy::redundant_closure_call)]
SendRetransmission(peer) => tx_maybe_with!(peer, || self
.crypt
.retransmit_handshake(peer.lower(), &mut *tx))?,
DeleteKey(peer) => {
self.output_key(peer, Stale, &SymKey::random())?;
// There was a loss of connection apparently; restart host discovery
// starting from the last used address but including all the initially
// specified addresses
// TODO: We could do this preemptively, before any connection loss actually occurs.
let p = peer.get_app_mut(self);
p.current_endpoint = Endpoint::discovery_from_multiple_sources(
p.current_endpoint.as_ref(),
p.initial_endpoint.as_ref(),
);
}
ReceivedMessage(len, endpoint) => {
match self.crypt.handle_msg(&rx[..len], &mut *tx) {
Err(ref e) => {
self.verbose().then(|| {
info!(
"error processing incoming message from {:?}: {:?} {}",
endpoint,
e,
e.backtrace()
);
});
}
Ok(HandleMsgResult {
resp,
exchanged_with,
..
}) => {
if let Some(len) = resp {
endpoint.send(self, &tx[0..len])?;
}
if let Some(p) = exchanged_with {
let ap = AppPeerPtr::lift(p);
ap.get_app_mut(self).current_endpoint = Some(endpoint);
// TODO: Maybe we should rather call the key "rosenpass output"?
self.output_key(ap, Exchanged, &self.crypt.osk(p)?)?;
}
}
}
}
};
}
}
pub fn output_key(
&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, fmt_b64(&*peerid));
}
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
// 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_writer(fopen_w(of)?).write_all(key.secret())?;
let why = match why {
KeyOutputReason::Exchanged => "exchanged",
KeyOutputReason::Stale => "stale",
};
// this is intentionally writing to stdout instead of stderr, because
// it is meant to allow external detection of a successful key-exchange
println!(
"output-key peer {} key-file {of:?} {why}",
fmt_b64(&*peerid)
);
}
if let Some(owg) = ap.outwg.as_ref() {
let mut child = Command::new("wg")
.arg("set")
.arg(&owg.dev)
.arg("peer")
.arg(&owg.pk)
.arg("preshared-key")
.arg("/dev/stdin")
.stdin(Stdio::piped())
.args(&owg.extra_params)
.spawn()?;
b64_writer(child.stdin.take().unwrap()).write_all(key.secret())?;
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(())
}
pub fn poll(&mut self, rx_buf: &mut [u8]) -> anyhow::Result<AppPollResult> {
use crate::protocol::PollResult as C;
use AppPollResult as A;
loop {
return Ok(match self.crypt.poll()? {
C::DeleteKey(PeerPtr(no)) => A::DeleteKey(AppPeerPtr(no)),
C::SendInitiation(PeerPtr(no)) => A::SendInitiation(AppPeerPtr(no)),
C::SendRetransmission(PeerPtr(no)) => A::SendRetransmission(AppPeerPtr(no)),
C::Sleep(timeout) => match self.try_recv(rx_buf, timeout)? {
Some((len, addr)) => A::ReceivedMessage(len, addr),
None => continue,
},
});
}
}
/// Tries to receive a new message
///
/// - might wait for an duration up to `timeout`
/// - returns immediately if an error occurs
/// - returns immediately if a new message is received
pub fn try_recv(
&mut self,
buf: &mut [u8],
timeout: Timing,
) -> anyhow::Result<Option<(usize, Endpoint)>> {
let timeout = Duration::from_secs_f64(timeout);
// if there is no time to wait on IO, well, then, lets not waste any time!
if timeout.is_zero() {
return Ok(None);
}
// NOTE when using mio::Poll, there are some particularities (taken from
// https://docs.rs/mio/latest/mio/struct.Poll.html):
//
// - poll() might return readiness, even if nothing is ready
// - in this case, a WouldBlock error is returned from actual IO operations
// - after receiving readiness for a source, it must be drained until a WouldBlock
// is received
//
// This would usually require us to maintain the drainage status of each socket;
// a socket would only become drained when it returned WouldBlock and only
// non-drained when receiving a readiness event from mio for it. Then, only the
// ready sockets should be worked on, ideally without requiring an O(n) search
// through all sockets for checking their drained status. However, our use-case
// is primarily heaving one or two sockets (if IPv4 and IPv6 IF_ANY listen is
// desired on a non-dual-stack OS), thus just checking every socket after any
// readiness event seems to be good enough™ for now.
// only poll if we drained all sockets before
if self.all_sockets_drained {
self.mio_poll.poll(&mut self.events, Some(timeout))?;
}
let mut would_block_count = 0;
for (sock_no, socket) in self.sockets.iter_mut().enumerate() {
match socket.recv_from(buf) {
Ok((n, addr)) => {
// at least one socket was not drained...
self.all_sockets_drained = false;
return Ok(Some((
n,
Endpoint::SocketBoundAddress {
socket: SocketPtr(sock_no),
addr,
},
)));
}
Err(e) if e.kind() == ErrorKind::WouldBlock => {
would_block_count += 1;
}
// TODO if one socket continuously returns an error, then we never poll, thus we never wait for a timeout, thus we have a spin-lock
Err(e) => return Err(e.into()),
}
}
// if each socket returned WouldBlock, then we drained them all at least once indeed
self.all_sockets_drained = would_block_count == self.sockets.len();
Ok(None)
}
}

View File

@@ -1,338 +0,0 @@
use anyhow::{bail, ensure};
use clap::{Parser, Subcommand};
use rosenpass_cipher_traits::Kem;
use rosenpass_ciphers::kem::StaticKem;
use rosenpass_secret_memory::file::StoreSecret;
use rosenpass_util::file::{LoadValue, LoadValueB64};
use std::path::PathBuf;
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>,
/// 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,
#[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)]
pub enum CliCommand {
/// Start Rosenpass in server mode and carry on with the key exchange
///
/// This will parse the configuration file and perform the key exchange
/// with the specified peers. If a peer's endpoint is specified, this
/// Rosenpass instance will try to initiate a key exchange with the peer,
/// otherwise only initiation attempts from the peer will be responded to.
ExchangeConfig { config_file: PathBuf },
/// Start in daemon mode, performing key exchanges
///
/// The configuration is read from the command line. The `peer` token
/// always separates multiple peers, e. g. if the token `peer` appears
/// in the WIREGUARD_EXTRA_ARGS it is not put into the WireGuard arguments
/// but instead a new peer is created.
/* Explanation: `first_arg` and `rest_of_args` are combined into one
* `Vec<String>`. They are only used to trick clap into displaying some
* guidance on the CLI usage.
*/
#[allow(rustdoc::broken_intra_doc_links)]
#[allow(rustdoc::invalid_html_tags)]
Exchange {
/// public-key <PATH> secret-key <PATH> [listen <ADDR>:<PORT>]... [verbose]
#[clap(value_name = "OWN_CONFIG")]
first_arg: String,
/// peer public-key <PATH> [ENDPOINT] [PSK] [OUTFILE] [WG]
///
/// ENDPOINT := endpoint <HOST/IP>:<PORT>
///
/// PSK := preshared-key <PATH>
///
/// OUTFILE := outfile <PATH>
///
/// WG := wireguard <WIREGUARD_DEV> <WIREGUARD_PEER> [WIREGUARD_EXTRA_ARGS]...
#[clap(value_name = "PEERS")]
rest_of_args: Vec<String>,
/// Save the parsed configuration to a file before starting the daemon
#[clap(short, long)]
config_file: Option<PathBuf>,
},
/// Generate a demo config file
GenConfig {
config_file: PathBuf,
/// Forcefully overwrite existing config file
#[clap(short, long)]
force: bool,
},
/// Generate the keys mentioned in a configFile
///
/// Generates secret- & public-key to their destination. If a config file
/// is provided then the key file destination is taken from there.
/// Otherwise the
GenKeys {
config_file: Option<PathBuf>,
/// where to write public-key to
#[clap(short, long)]
public_key: Option<PathBuf>,
/// where to write secret-key to
#[clap(short, long)]
secret_key: Option<PathBuf>,
/// Forcefully overwrite public- & secret-key file
#[clap(short, long)]
force: bool,
},
/// Deprecated - use gen-keys instead
#[allow(rustdoc::broken_intra_doc_links)]
#[allow(rustdoc::invalid_html_tags)]
Keygen {
// NOTE yes, the legacy keygen argument initially really accepted "privet-key", not "secret-key"!
/// public-key <PATH> private-key <PATH>
args: Vec<String>,
},
/// Validate a configuration
Validate { config_files: Vec<PathBuf> },
/// Show the rosenpass manpage
// TODO make this the default, but only after the manpage has been adjusted once the CLI stabilizes
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) -> anyhow::Result<()> {
use CliCommand::*;
match self {
Man => {
let man_cmd = std::process::Command::new("man")
.args(["1", "rosenpass"])
.status();
if !(man_cmd.is_ok() && man_cmd.unwrap().success()) {
println!(include_str!(env!("ROSENPASS_MAN")));
}
}
GenConfig { config_file, force } => {
ensure!(
force || !config_file.exists(),
"config file {config_file:?} already exists"
);
config::Rosenpass::example_config().store(config_file)?;
}
// Deprecated - use gen-keys instead
Keygen { args } => {
log::warn!("The 'keygen' command is deprecated. Please use the 'gen-keys' command instead.");
let mut public_key: Option<PathBuf> = None;
let mut secret_key: Option<PathBuf> = None;
// Manual arg parsing, since clap wants to prefix flags with "--"
let mut args = args.into_iter();
loop {
match (args.next().as_deref(), args.next()) {
(Some("private-key"), Some(opt)) | (Some("secret-key"), Some(opt)) => {
secret_key = Some(opt.into());
}
(Some("public-key"), Some(opt)) => {
public_key = Some(opt.into());
}
(Some(flag), _) => {
bail!("Unknown option `{}`", flag);
}
(_, _) => break,
};
}
if secret_key.is_none() {
bail!("private-key is required");
}
if public_key.is_none() {
bail!("public-key is required");
}
generate_and_save_keypair(secret_key.unwrap(), public_key.unwrap())?;
}
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) {
(Some(config_file), _, _) => {
ensure!(
config_file.exists(),
"config file {config_file:?} does not exist"
);
let config = config::Rosenpass::load(config_file)?;
(config.public_key, config.secret_key)
}
(_, Some(pkf), Some(skf)) => (pkf, skf),
_ => {
bail!("either a config-file or both public-key and secret-key file are required")
}
};
// check that we are not overriding something unintentionally
let mut problems = vec![];
if !force && pkf.is_file() {
problems.push(format!(
"public-key file {pkf:?} exist, refusing to overwrite it"
));
}
if !force && skf.is_file() {
problems.push(format!(
"secret-key file {skf:?} exist, refusing to overwrite it"
));
}
if !problems.is_empty() {
bail!(problems.join("\n"));
}
// generate the keys and store them in files
generate_and_save_keypair(skf, pkf)?;
}
ExchangeConfig { config_file } => {
ensure!(
config_file.exists(),
"config file '{config_file:?}' does not exist"
);
let config = config::Rosenpass::load(config_file)?;
config.validate()?;
Self::event_loop(config)?;
}
Exchange {
first_arg,
mut rest_of_args,
config_file,
} => {
rest_of_args.insert(0, first_arg);
let args = rest_of_args;
let mut config = config::Rosenpass::parse_args(args)?;
if let Some(p) = config_file {
config.store(&p)?;
config.config_file_path = p;
}
config.validate()?;
Self::event_loop(config)?;
}
Validate { config_files } => {
for file in config_files {
match config::Rosenpass::load(&file) {
Ok(config) => {
eprintln!("{file:?} is valid TOML and conforms to the expected schema");
match config.validate() {
Ok(_) => eprintln!("{file:?} has passed all logical checks"),
Err(_) => eprintln!("{file:?} contains logical errors"),
}
}
Err(e) => eprintln!("{file:?} is not valid: {e}"),
}
}
}
}
Ok(())
}
fn event_loop(config: config::Rosenpass) -> anyhow::Result<()> {
// load own keys
let sk = SSk::load(&config.secret_key)?;
let pk = SPk::load(&config.public_key)?;
// start an application server
let mut srv = std::boxed::Box::<AppServer>::new(AppServer::new(
sk,
pk,
config.listen,
config.verbosity,
)?);
for cfg_peer in config.peers {
srv.add_peer(
// psk, pk, outfile, outwg, tx_addr
cfg_peer.pre_shared_key.map(SymKey::load_b64).transpose()?,
SPk::load(&cfg_peer.public_key)?,
cfg_peer.key_out,
cfg_peer.wg.map(|cfg| app_server::WireguardOut {
dev: cfg.device,
pk: cfg.peer,
extra_params: cfg.extra_params,
}),
cfg_peer.endpoint.clone(),
)?;
}
srv.event_loop()
}
}
/// generate secret and public keys, store in files according to the paths passed as arguments
fn generate_and_save_keypair(secret_key: PathBuf, public_key: PathBuf) -> anyhow::Result<()> {
let mut ssk = crate::protocol::SSk::random();
let mut spk = crate::protocol::SPk::random();
StaticKem::keygen(ssk.secret_mut(), spk.secret_mut())?;
ssk.store_secret(secret_key)?;
spk.store_secret(public_key)
}

358
rosenpass/src/coloring.rs Normal file
View File

@@ -0,0 +1,358 @@
//! This module contains various types for dealing with secrets
//!
//! These types use type level coloring to make accidential leackage of secrets extra hard.
//!
use crate::{
sodium::{rng, zeroize},
util::{cpy, mutating},
};
use lazy_static::lazy_static;
use libsodium_sys as libsodium;
use std::{
collections::HashMap,
convert::TryInto,
fmt,
ops::{Deref, DerefMut},
os::raw::c_void,
ptr::null_mut,
sync::Mutex,
};
// 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…
lazy_static! {
static ref SECRET_CACHE: Mutex<SecretMemoryPool> = Mutex::new(SecretMemoryPool::new());
}
/// Pool that stores secret memory allocations
///
/// Allocation of secret memory is expensive. Thus, this struct provides a
/// pool of secret memory, readily available to yield protected, slices of
/// memory.
///
/// Further information about the protection in place can be found in in the
/// [libsodium documentation](https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations)
#[derive(Debug)] // TODO check on Debug derive, is that clever
pub struct SecretMemoryPool {
pool: HashMap<usize, Vec<*mut c_void>>,
}
impl SecretMemoryPool {
/// Create a new [SecretMemoryPool]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let pool = HashMap::new();
Self { pool }
}
/// Return secrete back to the pool for future re-use
///
/// This consumes the [Secret], but its memory is re-used.
pub fn release<const N: usize>(&mut self, mut s: Secret<N>) {
unsafe {
self.release_by_ref(&mut s);
}
std::mem::forget(s);
}
/// Return secret back to the pool for future re-use, by slice
///
/// # Safety
///
/// After calling this function on a [Secret], the secret must never be
/// used again for anything.
unsafe fn release_by_ref<const N: usize>(&mut self, s: &mut Secret<N>) {
s.zeroize();
let Secret { ptr: secret } = s;
// don't call Secret::drop, that could cause a double free
self.pool.entry(N).or_default().push(*secret);
}
/// Take protected memory from the pool, allocating new one if no suitable
/// chunk is found in the inventory.
///
/// The secret is guaranteed to be full of nullbytes
///
/// # Safety
///
/// This function contains an unsafe call to [libsodium::sodium_malloc].
/// This call has no known safety invariants, thus nothing can go wrong™.
/// However, just like normal `malloc()` this can return a null ptr. Thus
/// the returned pointer is checked for null; causing the program to panic
/// if it is null.
pub fn take<const N: usize>(&mut self) -> Secret<N> {
let entry = self.pool.entry(N).or_default();
let secret = entry.pop().unwrap_or_else(|| {
let ptr = unsafe { libsodium::sodium_malloc(N) };
assert!(
!ptr.is_null(),
"libsodium::sodium_mallloc() returned a null ptr"
);
ptr
});
let mut s = Secret { ptr: secret };
s.zeroize();
s
}
}
impl Drop for SecretMemoryPool {
/// # Safety
///
/// The drop implementation frees the contained elements using
/// [libsodium::sodium_free]. This is safe as long as every `*mut c_void`
/// contained was initialized with a call to [libsodium::sodium_malloc]
fn drop(&mut self) {
for ptr in self.pool.drain().flat_map(|(_, x)| x.into_iter()) {
unsafe {
libsodium::sodium_free(ptr);
}
}
}
}
/// # Safety
///
/// No safety implications are known, since the `*mut c_void` in
/// is essentially used like a `&mut u8` [SecretMemoryPool].
unsafe impl Send for SecretMemoryPool {}
/// Store for a secret
///
/// Uses memory allocated with [libsodium::sodium_malloc],
/// esentially can do the same things as `[u8; N].as_mut_ptr()`.
pub struct Secret<const N: usize> {
ptr: *mut c_void,
}
impl<const N: usize> Clone for Secret<N> {
fn clone(&self) -> Self {
let mut new = Self::zero();
new.secret_mut().clone_from_slice(self.secret());
new
}
}
impl<const N: usize> Drop for Secret<N> {
fn drop(&mut self) {
self.zeroize();
// the invariant that the [Secret] is not used after the
// `release_by_ref` call is guaranteed, since this is a drop implementation
unsafe { SECRET_CACHE.lock().unwrap().release_by_ref(self) };
self.ptr = null_mut();
}
}
impl<const N: usize> Secret<N> {
pub fn from_slice(slice: &[u8]) -> Self {
let mut new_self = Self::zero();
new_self.secret_mut().copy_from_slice(slice);
new_self
}
/// Returns a new [Secret] that is zero initialized
pub fn zero() -> Self {
// Using [SecretMemoryPool] here because this operation is expensive,
// yet it is used in hot loops
let s = SECRET_CACHE.lock().unwrap().take();
assert_eq!(s.secret(), &[0u8; N]);
s
}
/// Returns a new [Secret] that is randomized
pub fn random() -> Self {
mutating(Self::zero(), |r| r.randomize())
}
/// Sets all data of an existing secret to null bytes
pub fn zeroize(&mut self) {
zeroize(self.secret_mut());
}
/// Sets all data an existing secret to random bytes
pub fn randomize(&mut self) {
rng(self.secret_mut());
}
/// Borrows the data
pub fn secret(&self) -> &[u8; N] {
// - calling `from_raw_parts` is safe, because `ptr` is initalized with
// as `N` byte allocation from the creation of `Secret` onwards. `ptr`
// stays valid over the full lifetime of `Secret`
//
// - calling uwnrap is safe, because we can guarantee that the slice has
// exactly the required size `N` to create an array of `N` elements.
let ptr = self.ptr as *const u8;
let slice = unsafe { std::slice::from_raw_parts(ptr, N) };
slice.try_into().unwrap()
}
/// Borrows the data mutably
pub fn secret_mut(&mut self) -> &mut [u8; N] {
// the same safety argument as for `secret()` holds
let ptr = self.ptr as *mut u8;
let slice = unsafe { std::slice::from_raw_parts_mut(ptr, N) };
slice.try_into().unwrap()
}
}
/// The Debug implementation of [Secret] does not reveal the secret data,
/// instead a placeholder `<SECRET>` is used
impl<const N: usize> fmt::Debug for Secret<N> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str("<SECRET>")
}
}
/// Contains information in the form of a byte array that may be known to the
/// public
// TODO: We should get rid of the Public type; just use a normal value
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct Public<const N: usize> {
pub value: [u8; N],
}
impl<const N: usize> Public<N> {
/// Create a new [Public] from a byte slice
pub fn from_slice(value: &[u8]) -> Self {
mutating(Self::zero(), |r| cpy(value, &mut r.value))
}
/// Create a new [Public] from a byte array
pub fn new(value: [u8; N]) -> Self {
Self { value }
}
/// Create a zero initialized [Public]
pub fn zero() -> Self {
Self { value: [0u8; N] }
}
/// Create a random initialized [Public]
pub fn random() -> Self {
mutating(Self::zero(), |r| r.randomize())
}
/// Randomize all bytes in an existing [Public]
pub fn randomize(&mut self) {
rng(&mut self.value);
}
}
/// Writes the contents of an `&[u8]` as hexadecimal symbols to a [std::fmt::Formatter]
pub fn debug_crypto_array(v: &[u8], fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str("[{}]=")?;
if v.len() > 64 {
for byte in &v[..32] {
std::fmt::LowerHex::fmt(byte, fmt)?;
}
fmt.write_str("")?;
for byte in &v[v.len() - 32..] {
std::fmt::LowerHex::fmt(byte, fmt)?;
}
} else {
for byte in v {
std::fmt::LowerHex::fmt(byte, fmt)?;
}
}
Ok(())
}
impl<const N: usize> fmt::Debug for Public<N> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
debug_crypto_array(&self.value, fmt)
}
}
impl<const N: usize> Deref for Public<N> {
type Target = [u8; N];
fn deref(&self) -> &[u8; N] {
&self.value
}
}
impl<const N: usize> DerefMut for Public<N> {
fn deref_mut(&mut self) -> &mut [u8; N] {
&mut self.value
}
}
#[cfg(test)]
mod test {
use super::*;
/// https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations
/// promises us that allocated memory is initialized with this magic byte
const SODIUM_MAGIC_BYTE: u8 = 0xdb;
/// must be called before any interaction with libsodium
fn init() {
unsafe { libsodium_sys::sodium_init() };
}
/// checks that whe can malloc with libsodium
#[test]
fn sodium_malloc() {
init();
const N: usize = 8;
let ptr = unsafe { libsodium_sys::sodium_malloc(N) };
let mem = unsafe { std::slice::from_raw_parts(ptr as *mut u8, N) };
assert_eq!(mem, &[SODIUM_MAGIC_BYTE; N])
}
/// checks that whe can free with libsodium
#[test]
fn sodium_free() {
init();
const N: usize = 8;
let ptr = unsafe { libsodium_sys::sodium_malloc(N) };
unsafe { libsodium_sys::sodium_free(ptr) }
}
/// check that we can alloc using the magic pool
#[test]
fn secret_memory_pool_take() {
init();
const N: usize = 0x100;
let mut pool = SecretMemoryPool::new();
let secret: Secret<N> = pool.take();
assert_eq!(secret.secret(), &[0; N]);
}
/// check that a secrete lives, even if its [SecretMemoryPool] is deleted
#[test]
fn secret_memory_pool_drop() {
init();
const N: usize = 0x100;
let mut pool = SecretMemoryPool::new();
let secret: Secret<N> = pool.take();
std::mem::drop(pool);
assert_eq!(secret.secret(), &[0; N]);
}
/// check that a secrete can be reborn, freshly initialized with zero
#[test]
fn secret_memory_pool_release() {
init();
const N: usize = 1;
let mut pool = SecretMemoryPool::new();
let mut secret: Secret<N> = pool.take();
let old_secret_ptr = secret.ptr;
secret.secret_mut()[0] = 0x13;
pool.release(secret);
// now check that we get the same ptr
let new_secret: Secret<N> = pool.take();
assert_eq!(old_secret_ptr, new_secret.ptr);
// and that the secret was zeroized
assert_eq!(new_secret.secret(), &[0; N]);
}
}

View File

@@ -1,587 +0,0 @@
//! 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,
io::Write,
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs},
path::{Path, PathBuf},
};
use anyhow::{bail, ensure};
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)]
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>
#[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
///
/// 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
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"
config.config_file_path = p.as_ref().to_owned();
// return
Ok(config)
}
/// Write a config to a file
pub fn store<P: AsRef<Path>>(&self, p: P) -> anyhow::Result<()> {
let serialized_config =
toml::to_string_pretty(&self).expect("unable to serialize the default config");
fs::write(p, serialized_config)?;
Ok(())
}
/// 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)?;
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
ensure!(
self.public_key.is_file(),
"could not find public-key file {:?}: no such file",
self.public_key
);
// check the secret-key file exists
ensure!(
self.secret_key.is_file(),
"could not find secret-key file {:?}: no such file",
self.secret_key
);
for (i, peer) in self.peers.iter().enumerate() {
// check peer's public-key file exists
ensure!(
peer.public_key.is_file(),
"peer {i} public-key file {:?} does not exist",
peer.public_key
);
// check endpoint is usable
if let Some(addr) = peer.endpoint.as_ref() {
ensure!(
addr.to_socket_addrs().is_ok(),
"peer {i} endpoint {} can not be parsed to a socket address",
addr
);
}
// TODO warn if neither out_key nor exchange_command is defined
}
Ok(())
}
/// Creates a new configuration
pub fn new<P1: AsRef<Path>, P2: AsRef<Path>>(public_key: P1, secret_key: P2) -> Self {
Self {
public_key: PathBuf::from(public_key.as_ref()),
secret_key: PathBuf::from(secret_key.as_ref()),
listen: vec![],
verbosity: Verbosity::Quiet,
peers: vec![],
config_file_path: PathBuf::new(),
}
}
/// Add IPv4 __and__ IPv6 IF_ANY address to the listen interfaces
pub fn add_if_any(&mut self, port: u16) {
let ipv4_any = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port));
let ipv6_any = SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0),
port,
0,
0,
));
self.listen.push(ipv4_any);
self.listen.push(ipv6_any);
}
/// from chaotic args
/// Quest: the grammar is undecideable, what do we do here?
pub fn parse_args(args: Vec<String>) -> anyhow::Result<Self> {
let mut config = Self::new("", "");
#[derive(Debug, Hash, PartialEq, Eq)]
enum State {
Own,
OwnPublicKey,
OwnSecretKey,
OwnListen,
Peer,
PeerPsk,
PeerPublicKey,
PeerEndpoint,
PeerOutfile,
PeerWireguardDev,
PeerWireguardPeer,
PeerWireguardExtraArgs,
}
let mut already_set = HashSet::new();
// TODO idea: use config.peers.len() to give index of peer with conflicting argument
use State::*;
let mut state = Own;
let mut current_peer = None;
let p_exists = "a peer should exist by now";
let wg_exists = "a peer wireguard should exist by now";
for arg in args {
state = match (state, arg.as_str(), &mut current_peer) {
(Own, "public-key", None) => OwnPublicKey,
(Own, "secret-key", None) => OwnSecretKey,
(Own, "private-key", None) => {
log::warn!(
"the private-key argument is deprecated, please use secret-key instead"
);
OwnSecretKey
}
(Own, "listen", None) => OwnListen,
(Own, "verbose", None) => {
config.verbosity = Verbosity::Verbose;
Own
}
(Own, "peer", None) => {
ensure!(
already_set.contains(&OwnPublicKey),
"public-key file must be set"
);
ensure!(
already_set.contains(&OwnSecretKey),
"secret-key file must be set"
);
already_set.clear();
current_peer = Some(RosenpassPeer::default());
Peer
}
(OwnPublicKey, pk, None) => {
ensure!(
already_set.insert(OwnPublicKey),
"public-key was already set"
);
config.public_key = pk.into();
Own
}
(OwnSecretKey, sk, None) => {
ensure!(
already_set.insert(OwnSecretKey),
"secret-key was already set"
);
config.secret_key = sk.into();
Own
}
(OwnListen, l, None) => {
already_set.insert(OwnListen); // multiple listen directives are allowed
for socket_addr in l.to_socket_addrs()? {
config.listen.push(socket_addr);
}
Own
}
(Peer | PeerWireguardExtraArgs, "peer", maybe_peer @ Some(_)) => {
// TODO check current peer
// commit current peer, create a new one
config.peers.push(maybe_peer.take().expect(p_exists));
already_set.clear();
current_peer = Some(RosenpassPeer::default());
Peer
}
(Peer, "public-key", Some(_)) => PeerPublicKey,
(Peer, "endpoint", Some(_)) => PeerEndpoint,
(Peer, "preshared-key", Some(_)) => PeerPsk,
(Peer, "outfile", Some(_)) => PeerOutfile,
(Peer, "wireguard", Some(_)) => PeerWireguardDev,
(PeerPublicKey, pk, Some(peer)) => {
ensure!(
already_set.insert(PeerPublicKey),
"public-key was already set"
);
peer.public_key = pk.into();
Peer
}
(PeerEndpoint, e, Some(peer)) => {
ensure!(already_set.insert(PeerEndpoint), "endpoint was already set");
peer.endpoint = Some(e.to_owned());
Peer
}
(PeerPsk, psk, Some(peer)) => {
ensure!(already_set.insert(PeerEndpoint), "peer psk was already set");
peer.pre_shared_key = Some(psk.into());
Peer
}
(PeerOutfile, of, Some(peer)) => {
ensure!(
already_set.insert(PeerOutfile),
"peer outfile was already set"
);
peer.key_out = Some(of.into());
Peer
}
(PeerWireguardDev, dev, Some(peer)) => {
ensure!(
already_set.insert(PeerWireguardDev),
"peer wireguard-dev was already set"
);
assert!(peer.wg.is_none());
peer.wg = Some(WireGuard {
device: dev.to_string(),
..Default::default()
});
PeerWireguardPeer
}
(PeerWireguardPeer, p, Some(peer)) => {
ensure!(
already_set.insert(PeerWireguardPeer),
"peer wireguard-peer was already set"
);
peer.wg.as_mut().expect(wg_exists).peer = p.to_string();
PeerWireguardExtraArgs
}
(PeerWireguardExtraArgs, arg, Some(peer)) => {
peer.wg
.as_mut()
.expect(wg_exists)
.extra_params
.push(arg.to_string());
PeerWireguardExtraArgs
}
// error cases
(Own, x, None) => {
bail!("unrecognised argument {x}");
}
(Own | OwnPublicKey | OwnSecretKey | OwnListen, _, Some(_)) => {
panic!("current_peer is not None while in Own* state, this must never happen")
}
(State::Peer, arg, Some(_)) => {
bail!("unrecongnised argument {arg}");
}
(
Peer
| PeerEndpoint
| PeerOutfile
| PeerPublicKey
| PeerPsk
| PeerWireguardDev
| PeerWireguardPeer
| PeerWireguardExtraArgs,
_,
None,
) => {
panic!("got peer options but no peer was created")
}
};
}
if let Some(p) = current_peer {
// TODO ensure peer is propagated with sufficient information
config.peers.push(p);
}
Ok(config)
}
}
impl Rosenpass {
/// Generate an example configuration
pub fn example_config() -> Self {
let peer = RosenpassPeer {
public_key: "/path/to/rp-peer-public-key".into(),
endpoint: Some("my-peer.test:9999".into()),
key_out: Some("/path/to/rp-key-out.txt".into()),
pre_shared_key: Some("additional pre shared key".into()),
wg: Some(WireGuard {
device: "wirgeguard device e.g. wg0".into(),
peer: "wireguard public key".into(),
extra_params: vec!["passed to".into(), "wg set".into()],
}),
};
Self {
public_key: "/path/to/rp-public-key".into(),
secret_key: "/path/to/rp-secret-key".into(),
peers: vec![peer],
..Self::new("", "")
}
}
}
impl Default for Verbosity {
fn default() -> Self {
Self::Quiet
}
}
#[cfg(test)]
mod test {
use std::net::IpAddr;
use super::*;
fn split_str(s: &str) -> Vec<String> {
s.split(' ').map(|s| s.to_string()).collect()
}
#[test]
fn test_simple_cli_parse() {
let args = split_str(
"public-key /my/public-key secret-key /my/secret-key verbose \
listen 0.0.0.0:9999 peer public-key /peer/public-key endpoint \
peer.test:9999 outfile /peer/rp-out",
);
let config = Rosenpass::parse_args(args).unwrap();
assert_eq!(config.public_key, PathBuf::from("/my/public-key"));
assert_eq!(config.secret_key, PathBuf::from("/my/secret-key"));
assert_eq!(config.verbosity, Verbosity::Verbose);
assert_eq!(
&config.listen,
&vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 9999)]
);
assert_eq!(
config.peers,
vec![RosenpassPeer {
public_key: PathBuf::from("/peer/public-key"),
endpoint: Some("peer.test:9999".into()),
pre_shared_key: None,
key_out: Some(PathBuf::from("/peer/rp-out")),
..Default::default()
}]
)
}
#[test]
fn test_cli_parse_multiple_peers() {
let args = split_str(
"public-key /my/public-key secret-key /my/secret-key verbose \
peer public-key /peer-a/public-key endpoint \
peer.test:9999 outfile /peer-a/rp-out \
peer public-key /peer-b/public-key outfile /peer-b/rp-out",
);
let config = Rosenpass::parse_args(args).unwrap();
assert_eq!(config.public_key, PathBuf::from("/my/public-key"));
assert_eq!(config.secret_key, PathBuf::from("/my/secret-key"));
assert_eq!(config.verbosity, Verbosity::Verbose);
assert!(&config.listen.is_empty());
assert_eq!(
config.peers,
vec![
RosenpassPeer {
public_key: PathBuf::from("/peer-a/public-key"),
endpoint: Some("peer.test:9999".into()),
pre_shared_key: None,
key_out: Some(PathBuf::from("/peer-a/rp-out")),
..Default::default()
},
RosenpassPeer {
public_key: PathBuf::from("/peer-b/public-key"),
endpoint: None,
pre_shared_key: None,
key_out: Some(PathBuf::from("/peer-b/rp-out")),
..Default::default()
}
]
)
}
}
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");
}
}
}

View File

@@ -1,46 +0,0 @@
//! Pseudo Random Functions (PRFs) with a tree-like label scheme which
//! ensures their uniqueness
use anyhow::Result;
use rosenpass_ciphers::{hash_domain::HashDomain, KEY_LEN};
// TODO Use labels that can serve as identifiers
macro_rules! hash_domain_ns {
($base:ident, $name:ident, $($lbl:expr),* ) => {
pub fn $name() -> Result<HashDomain> {
let t = $base()?;
$( let t = t.mix($lbl.as_bytes())?; )*
Ok(t)
}
}
}
macro_rules! hash_domain {
($base:ident, $name:ident, $($lbl:expr),* ) => {
pub fn $name() -> Result<[u8; KEY_LEN]> {
let t = $base()?;
$( let t = t.mix($lbl.as_bytes())?; )*
Ok(t.into_value())
}
}
}
pub fn protocol() -> Result<HashDomain> {
HashDomain::zero().mix("Rosenpass v1 mceliece460896 Kyber512 ChaChaPoly1305 BLAKE2s".as_bytes())
}
hash_domain_ns!(protocol, mac, "mac");
hash_domain_ns!(protocol, cookie, "cookie");
hash_domain_ns!(protocol, peerid, "peer id");
hash_domain_ns!(protocol, biscuit_ad, "biscuit additional data");
hash_domain_ns!(protocol, ckinit, "chaining key init");
hash_domain_ns!(protocol, _ckextract, "chaining key extract");
hash_domain!(_ckextract, mix, "mix");
hash_domain!(_ckextract, hs_enc, "handshake encryption");
hash_domain!(_ckextract, ini_enc, "initiator handshake encryption");
hash_domain!(_ckextract, res_enc, "responder handshake encryption");
hash_domain_ns!(_ckextract, _user, "user");
hash_domain_ns!(_user, _rp, "rosenpass.eu");
hash_domain!(_rp, osk, "wireguard psk");

View File

@@ -0,0 +1,45 @@
use {
crate::{prftree::PrfTree, sodium::KEY_SIZE},
anyhow::Result,
};
pub fn protocol() -> Result<PrfTree> {
PrfTree::zero().mix("Rosenpass v1 mceliece460896 Kyber512 ChaChaPoly1305 BLAKE2s".as_bytes())
}
// TODO Use labels that can serve as idents
macro_rules! prflabel {
($base:ident, $name:ident, $($lbl:expr),* ) => {
pub fn $name() -> Result<PrfTree> {
let t = $base()?;
$( let t = t.mix($lbl.as_bytes())?; )*
Ok(t)
}
}
}
prflabel!(protocol, mac, "mac");
prflabel!(protocol, cookie, "cookie");
prflabel!(protocol, peerid, "peer id");
prflabel!(protocol, biscuit_ad, "biscuit additional data");
prflabel!(protocol, ckinit, "chaining key init");
prflabel!(protocol, _ckextract, "chaining key extract");
macro_rules! prflabel_leaf {
($base:ident, $name:ident, $($lbl:expr),* ) => {
pub fn $name() -> Result<[u8; KEY_SIZE]> {
let t = $base()?;
$( let t = t.mix($lbl.as_bytes())?; )*
Ok(t.into_value())
}
}
}
prflabel_leaf!(_ckextract, mix, "mix");
prflabel_leaf!(_ckextract, hs_enc, "handshake encryption");
prflabel_leaf!(_ckextract, ini_enc, "initiator handshake encryption");
prflabel_leaf!(_ckextract, res_enc, "responder handshake encryption");
prflabel!(_ckextract, _user, "user");
prflabel!(_user, _rp, "rosenpass.eu");
prflabel_leaf!(_rp, osk, "wireguard psk");

View File

@@ -1,14 +1,56 @@
pub mod app_server;
pub mod cli;
pub mod config;
pub mod hash_domains;
#[macro_use]
pub mod util;
#[macro_use]
pub mod sodium;
pub mod coloring;
pub mod labeled_prf;
pub mod msgs;
pub mod pqkem;
pub mod prftree;
pub mod protocol;
#[derive(thiserror::Error, Debug)]
pub enum RosenpassError {
#[error("buffer size mismatch")]
BufferSizeMismatch,
#[error("error in OQS")]
Oqs,
#[error("error from external library while calling OQS")]
OqsExternalLib,
#[error("buffer size mismatch, required {required_size} but only found {actual_size}")]
BufferSizeMismatch {
required_size: usize,
actual_size: usize,
},
#[error("invalid message type")]
InvalidMessageType(u8),
}
impl RosenpassError {
/// Helper function to check a buffer size
fn check_buffer_size(required_size: usize, actual_size: usize) -> Result<(), Self> {
if required_size != actual_size {
Err(Self::BufferSizeMismatch {
required_size,
actual_size,
})
} else {
Ok(())
}
}
}
/// Extension trait to attach function calls to foreign types.
trait RosenpassMaybeError {
/// Checks whether something is an error or not
fn to_rg_error(&self) -> Result<(), RosenpassError>;
}
impl RosenpassMaybeError for oqs_sys::common::OQS_STATUS {
fn to_rg_error(&self) -> Result<(), RosenpassError> {
use oqs_sys::common::OQS_STATUS;
match self {
OQS_STATUS::OQS_SUCCESS => Ok(()),
OQS_STATUS::OQS_ERROR => Err(RosenpassError::Oqs),
OQS_STATUS::OQS_EXTERNAL_LIB_ERROR_OPENSSL => Err(RosenpassError::OqsExternalLib),
}
}
}

106
rosenpass/src/lprf.rs Normal file
View File

@@ -0,0 +1,106 @@
//! The rosenpass protocol relies on a special type
//! of hash function for most of its hashing or
//! message authentication needs: an incrementable
//! pseudo random function.
//!
//! This is a generalization of a PRF operating
//! on a sequence of inputs instead of a single input.
//!
//! Like a Dec function the Iprf features efficient
//! incrementability.
//!
//! You can also think of an Iprf as a Dec function with
//! a fixed size output.
//!
//! The idea behind a Iprf is that it can be efficiently
//! constructed from an Dec function as well as a PRF.
//!
//! TODO Base the construction on a proper Dec function
pub struct Iprf([u8; KEY_SIZE]);
pub struct IprfBranch([u8; KEY_SIZE]);
pub struct SecretIprf(Secret<KEY_SIZE>);
pub struct SecretIprfBranch(Secret<KEY_SIZE>);
pub fn prf_into(out: &mut [u8], key: &[u8], data: &[u8]) {
// TODO: The error handling with sodium is a scurge
hmac_into(out, key, data).unwrap()
}
pub fn prf(key: &[u8], data: &[u8]) -> [u8; KEY_SIZE]{
mutating([0u8; KEY_SIZE], |r| prf_into(r, key, data))
}
impl Iprf {
fn zero() -> Self {
Self([0u8; KEY_SIZE])
}
fn dup(self) -> IprfBranch {
IprfBranch(self.0)
}
// TODO: Protocol! Use domain separation to ensure that
fn mix(self, v: &[u8]) -> Self {
Self(prf(&self.0, v))
}
fn mix_secret<const N: usize>(self, v: Secret<N>) -> SecretIprf {
SecretIprf::prf_invoc(&self.0, v.secret())
}
fn into_value(self) -> [u8; KEY_SIZE] {
self.0
}
fn extract(self, v: &[u8], dst: &mut [u8]) {
prf_into(&self.0, v, dst)
}
}
impl IprfBranch {
fn mix(&self, v: &[u8]) -> Iprf {
Iprf(prf(self.0, v))
}
fn mix_secret<const N: usize>(&self, v: Secret<N>) -> SecretIprf {
SecretIprf::prf_incov(self.0, v.secret())
}
}
impl SecretIprf {
fn prf_invoc(k: &[u8], d: &[u8]) -> SecretIprf {
mutating(SecretIprf(Secret::zero()), |r|
prf_into(k, d, r.secret_mut()))
}
fn from_key(k: Secret<N>) -> SecretIprf {
Self(k)
}
fn mix(self, v: &[u8]) -> SecretIprf {
Self::prf_invoc(self.0.secret(), v)
}
fn mix_secret<const N: usize>(self, v: Secret<N>) -> SecretIprf {
Self::prf_invoc(self.0.secret(), v.secret())
}
fn into_secret(self) -> Secret<KEY_SIZE> {
self.0
}
fn into_secret_slice(self, v: &[u8], dst: &[u8]) {
prf_into(self.0.secret(), v, dst)
}
}
impl SecretIprfBranch {
fn mix(&self, v: &[u8]) -> SecretIprf {
SecretIprf::prf_invoc(self.0.secret(), v)
}
fn mix_secret<const N: usize>(&self, v: Secret<N>) -> SecretIprf {
SecretIprf::prf_invoc(self.0.secret(), v.secret())
}
}

View File

@@ -1,32 +1,262 @@
use clap::Parser;
use log::error;
use rosenpass::cli::CliArgs;
use std::process::exit;
use anyhow::{bail, ensure, Context, Result};
use log::{error, info};
use rosenpass::{
attempt,
coloring::{Public, Secret},
multimatch,
pqkem::{SKEM, KEM},
protocol::{SPk, SSk, MsgBuf, PeerPtr, Server as CryptoServer, SymKey, Timing},
sodium::sodium_init,
util::{b64_reader, b64_writer, fmt_b64},
};
use std::{
fs::{File, OpenOptions},
io::{ErrorKind, Read, Write},
net::{SocketAddr, ToSocketAddrs, UdpSocket},
path::Path,
process::{exit, Command, Stdio},
time::Duration,
};
/// Open a file writable
pub fn fopen_w<P: AsRef<Path>>(path: P) -> Result<File> {
Ok(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) -> Result<File> {
Ok(OpenOptions::new()
.read(true)
.write(false)
.create(false)
.truncate(false)
.open(path)?)
}
pub trait ReadExactToEnd {
fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()>;
}
impl<R: Read> ReadExactToEnd for R {
fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()> {
let mut dummy = [0u8; 8];
self.read_exact(buf)?;
ensure!(self.read(&mut dummy)? == 0, "File too long!");
Ok(())
}
}
pub trait LoadValue {
fn load<P: AsRef<Path>>(path: P) -> Result<Self>
where
Self: Sized;
}
pub trait LoadValueB64 {
fn load_b64<P: AsRef<Path>>(path: P) -> Result<Self>
where
Self: Sized;
}
trait StoreValue {
fn store<P: AsRef<Path>>(&self, path: P) -> Result<()>;
}
trait StoreSecret {
unsafe fn store_secret<P: AsRef<Path>>(&self, path: P) -> Result<()>;
}
impl<T: StoreValue> StoreSecret for T {
unsafe fn store_secret<P: AsRef<Path>>(&self, path: P) -> Result<()> {
self.store(path)
}
}
impl<const N: usize> LoadValue for Secret<N> {
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut v = Self::random();
let p = path.as_ref();
fopen_r(p)?
.read_exact_to_end(v.secret_mut())
.with_context(|| format!("Could not load file {p:?}"))?;
Ok(v)
}
}
impl<const N: usize> LoadValueB64 for Secret<N> {
fn load_b64<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut v = Self::random();
let p = path.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
// 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> StoreSecret for Secret<N> {
unsafe fn store_secret<P: AsRef<Path>>(&self, path: P) -> Result<()> {
std::fs::write(path, self.secret())?;
Ok(())
}
}
impl<const N: usize> LoadValue for Public<N> {
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut v = Self::random();
fopen_r(path)?.read_exact_to_end(&mut *v)?;
Ok(v)
}
}
impl<const N: usize> StoreValue for Public<N> {
fn store<P: AsRef<Path>>(&self, path: P) -> Result<()> {
std::fs::write(path, **self)?;
Ok(())
}
}
macro_rules! bail_usage {
($args:expr, $($pt:expr),*) => {{
error!($($pt),*);
cmd_help()?;
exit(1);
}}
}
macro_rules! ensure_usage {
($args:expr, $ck:expr, $($pt:expr),*) => {{
if !$ck {
bail_usage!($args, $($pt),*);
}
}}
}
macro_rules! mandatory_opt {
($args:expr, $val:expr, $name:expr) => {{
ensure_usage!($args, $val.is_some(), "{0} option is mandatory", $name)
}};
}
pub struct ArgsWalker {
pub argv: Vec<String>,
pub off: usize,
}
impl ArgsWalker {
pub fn get(&self) -> Option<&str> {
self.argv.get(self.off).map(|s| s as &str)
}
pub fn prev(&mut self) -> Option<&str> {
assert!(self.off > 0);
self.off -= 1;
self.get()
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<&str> {
assert!(self.todo() > 0);
self.off += 1;
self.get()
}
pub fn opt(&mut self, dst: &mut Option<String>) -> Result<()> {
let cmd = &self.argv[self.off - 1];
ensure_usage!(&self, self.todo() > 0, "Option {} takes a value", cmd);
ensure_usage!(&self, dst.is_none(), "Cannot set {} multiple times.", cmd);
*dst = Some(String::from(self.next().unwrap()));
Ok(())
}
fn todo(&self) -> usize {
self.argv.len() - self.off
}
}
#[derive(Default, Debug)]
pub struct WireguardOut {
// impl KeyOutput
dev: String,
pk: String,
extra_params: Vec<String>,
}
#[derive(Default, Debug)]
pub struct AppPeer {
pub outfile: Option<String>,
pub outwg: Option<WireguardOut>,
pub tx_addr: Option<SocketAddr>,
}
#[derive(Debug)]
pub enum Verbosity {
Quiet,
Verbose,
}
/// Holds the state of the application, namely the external IO
#[derive(Debug)]
pub struct AppServer {
pub crypt: CryptoServer,
pub sock: UdpSocket,
pub peers: Vec<AppPeer>,
pub verbosity: Verbosity,
}
/// Index based pointer to a Peer
#[derive(Debug)]
pub struct AppPeerPtr(pub usize);
impl AppPeerPtr {
/// Takes an index based handle and returns the actual peer
pub fn lift(p: PeerPtr) -> Self {
Self(p.0)
}
/// Returns an index based handle to one Peer
pub fn lower(&self) -> PeerPtr {
PeerPtr(self.0)
}
pub fn get_app<'a>(&self, srv: &'a AppServer) -> &'a AppPeer {
&srv.peers[self.0]
}
pub fn get_app_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut AppPeer {
&mut srv.peers[self.0]
}
}
#[derive(Debug)]
pub enum AppPollResult {
DeleteKey(AppPeerPtr),
SendInitiation(AppPeerPtr),
SendRetransmission(AppPeerPtr),
ReceivedMessage(usize, SocketAddr),
}
#[derive(Debug)]
pub enum KeyOutputReason {
Exchanged,
Stale,
}
/// Catches errors, prints them through the logger, then exits
pub fn main() {
// parse CLI arguments
let args = CliArgs::parse();
// 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() {
env_logger::init();
match rosenpass_main() {
Ok(_) => {}
Err(e) => {
error!("{e}");
@@ -34,3 +264,383 @@ pub fn main() {
}
}
}
/// Entry point to the whole program
pub fn rosenpass_main() -> Result<()> {
sodium_init()?;
let mut args = ArgsWalker {
argv: std::env::args().collect(),
off: 0, // skipping executable path
};
// Command parsing
match args.next() {
Some("help") | Some("-h") | Some("-help") | Some("--help") => cmd_help()?,
Some("keygen") => cmd_keygen(args)?,
Some("exchange") => cmd_exchange(args)?,
Some(cmd) => bail_usage!(&args, "No such command {}", cmd),
None => bail_usage!(&args, "Expected a command!"),
};
Ok(())
}
/// Print the usage information
pub fn cmd_help() -> Result<()> {
eprint!(include_str!("usage.md"), env!("CARGO_BIN_NAME"));
Ok(())
}
/// Generate a keypair
pub fn cmd_keygen(mut args: ArgsWalker) -> Result<()> {
let mut sf: Option<String> = None;
let mut pf: Option<String> = None;
// Arg parsing
loop {
match args.next() {
Some("private-key") => args.opt(&mut sf)?,
Some("public-key") => args.opt(&mut pf)?,
Some(opt) => bail_usage!(&args, "Unknown option `{}`", opt),
None => break,
};
}
mandatory_opt!(&args, sf, "private-key");
mandatory_opt!(&args, pf, "private-key");
// Cmd
let (mut ssk, mut spk) = (SSk::random(), SPk::random());
unsafe {
SKEM::keygen(ssk.secret_mut(), spk.secret_mut())?;
ssk.store_secret(sf.unwrap())?;
spk.store_secret(pf.unwrap())?;
}
Ok(())
}
pub fn cmd_exchange(mut args: ArgsWalker) -> Result<()> {
// Argument parsing
let mut sf: Option<String> = None;
let mut pf: Option<String> = None;
let mut listen: Option<String> = None;
let mut verbosity = Verbosity::Quiet;
// Global parameters
loop {
match args.next() {
Some("private-key") => args.opt(&mut sf)?,
Some("public-key") => args.opt(&mut pf)?,
Some("listen") => args.opt(&mut listen)?,
Some("verbose") => {
verbosity = Verbosity::Verbose;
}
Some("peer") => {
args.prev();
break;
}
Some(opt) => bail_usage!(&args, "Unknown option `{}`", opt),
None => break,
};
}
mandatory_opt!(&args, sf, "private-key");
mandatory_opt!(&args, pf, "public-key");
let mut srv = std::boxed::Box::<AppServer>::new(AppServer::new(
// sk, pk, addr
SSk::load(&sf.unwrap())?,
SPk::load(&pf.unwrap())?,
listen.as_deref().unwrap_or("[0::0]:0"),
verbosity,
)?);
// Peer parameters
'_parseAllPeers: while args.todo() > 0 {
let mut pf: Option<String> = None;
let mut outfile: Option<String> = None;
let mut outwg: Option<WireguardOut> = None;
let mut endpoint: Option<String> = None;
let mut pskf: Option<String> = None;
args.next(); // skip "peer" starter itself
'parseOnePeer: loop {
match args.next() {
// Done with this peer
Some("peer") => {
args.prev();
break 'parseOnePeer;
}
None => break 'parseOnePeer,
// Options
Some("public-key") => args.opt(&mut pf)?,
Some("endpoint") => args.opt(&mut endpoint)?,
Some("preshared-key") => args.opt(&mut pskf)?,
Some("outfile") => args.opt(&mut outfile)?,
// Wireguard out
Some("wireguard") => {
ensure_usage!(
&args,
outwg.is_none(),
"Cannot set wireguard output for the same peer multiple times."
);
ensure_usage!(&args, args.todo() >= 2, "Option wireguard takes to values");
let dev = String::from(args.next().unwrap());
let pk = String::from(args.next().unwrap());
let wg = outwg.insert(WireguardOut {
dev,
pk,
extra_params: Vec::new(),
});
'_parseWgOutExtra: loop {
match args.next() {
Some("peer") => {
args.prev();
break 'parseOnePeer;
}
None => break 'parseOnePeer,
Some(xtra) => wg.extra_params.push(xtra.to_string()),
};
}
}
// Invalid
Some(opt) => bail_usage!(&args, "Unknown peer option `{}`", opt),
};
}
mandatory_opt!(&args, pf, "private-key");
ensure_usage!(
&args,
outfile.is_some() || outwg.is_some(),
"Either of the outfile or wireguard option is mandatory"
);
let tx_addr = endpoint
.map(|e| {
e.to_socket_addrs()?
.next()
.context("Expected address in endpoint parameter")
})
.transpose()?;
srv.add_peer(
// psk, pk, outfile, outwg, tx_addr
pskf.map(SymKey::load_b64).transpose()?,
SPk::load(&pf.unwrap())?,
outfile,
outwg,
tx_addr,
)?;
}
srv.listen_loop()
}
impl AppServer {
pub fn new<A: ToSocketAddrs>(
sk: SSk,
pk: SPk,
addr: A,
verbosity: Verbosity,
) -> Result<Self> {
Ok(Self {
crypt: CryptoServer::new(sk, pk),
sock: UdpSocket::bind(addr)?,
peers: Vec::new(),
verbosity,
})
}
pub fn verbose(&self) -> bool {
matches!(self.verbosity, Verbosity::Verbose)
}
pub fn add_peer(
&mut self,
psk: Option<SymKey>,
pk: SPk,
outfile: Option<String>,
outwg: Option<WireguardOut>,
tx_addr: Option<SocketAddr>,
) -> Result<AppPeerPtr> {
let PeerPtr(pn) = self.crypt.add_peer(psk, pk)?;
assert!(pn == self.peers.len());
self.peers.push(AppPeer {
outfile,
outwg,
tx_addr,
});
Ok(AppPeerPtr(pn))
}
pub fn listen_loop(&mut self) -> Result<()> {
const INIT_SLEEP: f64 = 0.01;
const MAX_FAILURES: i32 = 10;
let mut failure_cnt = 0;
loop {
let msgs_processed = 0usize;
let err = match self.event_loop() {
Ok(()) => return Ok(()),
Err(e) => e,
};
// This should not happen…
failure_cnt = if msgs_processed > 0 {
0
} else {
failure_cnt + 1
};
let sleep = INIT_SLEEP * 2.0f64.powf(f64::from(failure_cnt - 1));
let tries_left = MAX_FAILURES - (failure_cnt - 1);
error!(
"unexpected error after processing {} messages: {:?} {}",
msgs_processed,
err,
err.backtrace()
);
if tries_left > 0 {
error!("reinitializing networking in {sleep}! {tries_left} tries left.");
std::thread::sleep(self.crypt.timebase.dur(sleep));
continue;
}
bail!("too many network failures");
}
}
pub fn event_loop(&mut self) -> Result<()> {
let (mut rx, mut tx) = (MsgBuf::zero(), MsgBuf::zero());
macro_rules! tx_maybe_with {
($peer:expr, $fn:expr) => {
attempt!({
let p = $peer.get_app(self);
if let Some(addr) = p.tx_addr {
let len = $fn()?;
self.sock.send_to(&tx[..len], addr)?;
}
Ok(())
})
};
}
loop {
use rosenpass::protocol::HandleMsgResult;
use AppPollResult::*;
use KeyOutputReason::*;
match self.poll(&mut *rx)? {
SendInitiation(peer) => tx_maybe_with!(peer, || self
.crypt
.initiate_handshake(peer.lower(), &mut *tx))?,
SendRetransmission(peer) => tx_maybe_with!(peer, || self
.crypt
.retransmit_handshake(peer.lower(), &mut *tx))?,
DeleteKey(peer) => self.output_key(peer, Stale, &SymKey::random())?,
ReceivedMessage(len, addr) => {
multimatch!(self.crypt.handle_msg(&rx[..len], &mut *tx),
Err(ref e) =>
self.verbose().then(||
info!("error processing incoming message from {:?}: {:?} {}", addr, e, e.backtrace())),
Ok(HandleMsgResult { resp: Some(len), .. }) => {
self.sock.send_to(&tx[0..len], addr)?
},
Ok(HandleMsgResult { exchanged_with: Some(p), .. }) => {
let ap = AppPeerPtr::lift(p);
ap.get_app_mut(self).tx_addr = Some(addr);
// TODO: Maybe we should rather call the key "rosenpass output"?
self.output_key(ap, Exchanged, &self.crypt.osk(p)?)?;
}
);
}
};
}
}
pub fn output_key(&self, peer: AppPeerPtr, why: KeyOutputReason, key: &SymKey) -> 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, fmt_b64(&*peerid));
}
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
// 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_writer(fopen_w(of)?).write_all(key.secret())?;
let why = match why {
KeyOutputReason::Exchanged => "exchanged",
KeyOutputReason::Stale => "stale",
};
println!(
"output-key peer {} key-file {} {}",
fmt_b64(&*peerid),
of,
why
);
}
if let Some(owg) = ap.outwg.as_ref() {
let child = Command::new("wg")
.arg("set")
.arg(&owg.dev)
.arg("peer")
.arg(&owg.pk)
.arg("preshared-key")
.arg("/dev/stdin")
.stdin(Stdio::piped())
.args(&owg.extra_params)
.spawn()?;
b64_writer(child.stdin.unwrap()).write_all(key.secret())?;
}
Ok(())
}
pub fn poll(&mut self, rx_buf: &mut [u8]) -> Result<AppPollResult> {
use rosenpass::protocol::PollResult as C;
use AppPollResult as A;
loop {
return Ok(match self.crypt.poll()? {
C::DeleteKey(PeerPtr(no)) => A::DeleteKey(AppPeerPtr(no)),
C::SendInitiation(PeerPtr(no)) => A::SendInitiation(AppPeerPtr(no)),
C::SendRetransmission(PeerPtr(no)) => A::SendRetransmission(AppPeerPtr(no)),
C::Sleep(timeout) => match self.try_recv(rx_buf, timeout)? {
Some((len, addr)) => A::ReceivedMessage(len, addr),
None => continue,
},
});
}
}
pub fn try_recv(&self, buf: &mut [u8], timeout: Timing) -> Result<Option<(usize, SocketAddr)>> {
if timeout == 0.0 {
return Ok(None);
}
self.sock
.set_read_timeout(Some(Duration::from_secs_f64(timeout)))?;
match self.sock.recv_from(buf) {
Ok(x) => Ok(Some(x)),
Err(e) => match e.kind() {
ErrorKind::WouldBlock => Ok(None),
ErrorKind::TimedOut => Ok(None),
_ => Err(anyhow::Error::new(e)),
},
}
}
}

View File

@@ -1,113 +1,308 @@
//! Data structures representing the messages going over the wire
//! # Messages
//!
//! This module contains de-/serialization of the protocol's messages. Thats kind
//! of a lie, since no actual ser/de happens. Instead, the structures offer views
//! into mutable byte slices (`&mut [u8]`), allowing to modify the fields of an
//! This module contains data structures that help in the
//! serialization/deserialization (ser/de) of messages. Thats kind of a lie,
//! since no actual ser/de happens. Instead, the structures offer views into
//! mutable byte slices (`&mut [u8]`), allowing to modify the fields of an
//! 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.
//!
//! # Example
//!
//! The following example uses the [`data_lense` macro](crate::data_lense) to create a lense that
//! might be useful when dealing with UDP headers.
//!
//! ```
//! use rosenpass::{data_lense, RosenpassError, msgs::LenseView};
//! # fn main() -> Result<(), RosenpassError> {
//!
//! data_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};
use std::mem::size_of;
use zerocopy::{AsBytes, FromBytes, FromZeroes};
use crate::{pqkem::*, sodium};
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct Envelope<M: AsBytes + FromBytes> {
// Macro magic ////////////////////////////////////////////////////////////////
/// A macro to create data lenses. Refer to the [`msgs` mod](crate::msgs) for
/// an example and further elaboration
// TODO implement TryFrom<[u8]> and From<[u8; Self::len()]>
#[macro_export]
macro_rules! data_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 = data_lense!(maybe_docstring_link $len)]
/// bytes long
pub fn $field(&self) -> &__ContainerType::Output {
&self.0[$offset .. $offset + $len]
}
/// The bytes until the
#[doc = data_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
$(
data_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 = data_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
$(
data_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 = data_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` is sufficiently long to hold [Self]
pub fn check_size(len: usize) -> Result<(), RosenpassError>{
let required_size = $( $len + )+ 0;
let actual_size = len;
if required_size < actual_size {
Err(RosenpassError::BufferSizeMismatch {
required_size,
actual_size,
})
}else{
Ok(())
}
}
}
// read-only accessor functions
impl<'a, __ContainerType $(, $( $generic: LenseView ),+ )?> $type<&'a __ContainerType $(, $( $generic ),+ )?>
where
__ContainerType: std::ops::Index<std::ops::Range<usize>> + ?Sized,
{
data_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,
{
data_lense!{token_muncher_ref @ 0 ; $( $( $attr )* ; $field : $len ),+ }
data_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 = data_lense!(maybe_docstring_link $type)]
pub trait [< $type Ext >] {
type __ContainerType;
/// Create a lense to the byte slice
fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type<Self::__ContainerType, $( $($generic),+ )? >, RosenpassError>;
}
impl<'a> [< $type Ext >] for &'a [u8] {
type __ContainerType = &'a [u8];
fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type<Self::__ContainerType, $( $($generic),+ )? >, RosenpassError> {
Ok($type ( self, $( $( ::core::marker::PhantomData::<$generic> ),+ )? ))
}
}
impl<'a> [< $type Ext >] for &'a mut [u8] {
type __ContainerType = &'a mut [u8];
fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type<Self::__ContainerType, $( $($generic),+ )? >, RosenpassError> {
Ok($type ( self, $( $( ::core::marker::PhantomData::<$generic> ),+ )? ))
}
}
});
);
/// Common trait shared by all Lenses
pub trait LenseView {
const LEN: usize;
}
data_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: sodium::MAC_SIZE,
/// Currently unused, TODO: do something with this
pub cookie: [u8; 16],
cookie: sodium::MAC_SIZE
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct InitHello {
data_lense! { InitHello :=
/// Randomly generated connection id
pub sidi: [u8; 4],
sidi: 4,
/// Kyber 512 Ephemeral Public Key
pub epki: [u8; EphemeralKem::PK_LEN],
epki: EKEM::PK_LEN,
/// Classic McEliece Ciphertext
pub sctr: [u8; StaticKem::CT_LEN],
sctr: SKEM::CT_LEN,
/// Encryped: 16 byte hash of McEliece initiator static key
pub pidic: [u8; aead::TAG_LEN + 32],
pidic: sodium::AEAD_TAG_LEN + 32,
/// Encrypted TAI64N Time Stamp (against replay attacks)
pub auth: [u8; aead::TAG_LEN],
auth: sodium::AEAD_TAG_LEN
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct RespHello {
data_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: EKEM::CT_LEN,
/// Classic McEliece Ciphertext
pub scti: [u8; StaticKem::CT_LEN],
scti: SKEM::CT_LEN,
/// Empty encrypted message (just an auth tag)
pub auth: [u8; aead::TAG_LEN],
auth: sodium::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 {
data_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: sodium::AEAD_TAG_LEN
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct EmptyData {
data_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: sodium::AEAD_TAG_LEN
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct Biscuit {
data_lense! { Biscuit :=
/// H(spki) Ident ifies the initiator
pub pidi: [u8; KEY_LEN],
pidi: sodium::KEY_SIZE,
/// The biscuit number (replay protection)
pub biscuit_no: [u8; 12],
biscuit_no: 12,
/// Chaining key
pub ck: [u8; KEY_LEN],
ck: sodium::KEY_SIZE
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct DataMsg {
pub dummy: [u8; 4],
data_lense! { DataMsg :=
dummy: 4
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct CookieReply {
pub dummy: [u8; 4],
data_lense! { CookieReply :=
dummy: 4
}
// Traits /////////////////////////////////////////////////////////////////////
@@ -157,31 +352,33 @@ impl TryFrom<u8> for MsgType {
}
/// 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;
pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + sodium::XAEAD_NONCE_LEN + sodium::XAEAD_TAG_LEN;
#[cfg(test)]
mod test_constants {
use crate::msgs::{BISCUIT_CT_LEN, BISCUIT_PT_LEN};
use rosenpass_ciphers::{xaead, KEY_LEN};
use crate::{
msgs::{BISCUIT_CT_LEN, BISCUIT_PT_LEN},
sodium,
};
#[test]
fn sodium_keysize() {
assert_eq!(KEY_LEN, 32);
assert_eq!(sodium::KEY_SIZE, 32);
}
#[test]
fn biscuit_pt_len() {
assert_eq!(BISCUIT_PT_LEN, 2 * KEY_LEN + 12);
assert_eq!(BISCUIT_PT_LEN, 2 * sodium::KEY_SIZE + 12);
}
#[test]
fn biscuit_ct_len() {
assert_eq!(
BISCUIT_CT_LEN,
BISCUIT_PT_LEN + xaead::NONCE_LEN + xaead::TAG_LEN
BISCUIT_PT_LEN + sodium::XAEAD_NONCE_LEN + sodium::XAEAD_TAG_LEN
);
}
}

176
rosenpass/src/pqkem.rs Normal file
View File

@@ -0,0 +1,176 @@
//! This module contains Traits and implementations for Key Encapsulation
//! Mechanisms (KEM). KEMs are the interface provided by almost all post-quantum
//! secure key exchange mechanisms.
//!
//! Conceptually KEMs are akin to public-key encryption, but instead of encrypting
//! arbitrary data, KEMs are limited to the transmission of keys, randomly chosen during
//!
//! encapsulation.
//! The [KEM] Trait describes the basic API offered by a Key Encapsulation
//! Mechanism. Two implementations for it are provided, [SKEM] and [EKEM].
use crate::{RosenpassError, RosenpassMaybeError};
/// Key Encapsulation Mechanism
///
/// The KEM interface defines three operations: Key generation, key encapsulation and key
/// decapsulation.
pub trait KEM {
/// Secrete Key length
const SK_LEN: usize;
/// Public Key length
const PK_LEN: usize;
/// Ciphertext length
const CT_LEN: usize;
/// Shared Secret length
const SHK_LEN: usize;
/// Generate a keypair consisting of secret key (`sk`) and public key (`pk`)
///
/// `keygen() -> sk, pk`
fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError>;
/// From a public key (`pk`), generate a shared key (`shk`, for local use)
/// and a cipher text (`ct`, to be sent to the owner of the `pk`).
///
/// `encaps(pk) -> shk, ct`
fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError>;
/// From a secret key (`sk`) and a cipher text (`ct`) derive a shared key
/// (`shk`)
///
/// `decaps(sk, ct) -> shk`
fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError>;
}
/// A KEM that is secure against Chosen Ciphertext Attacks (CCA).
/// In the context of rosenpass this is used for static keys.
/// Uses [Classic McEliece](https://classic.mceliece.org/) 460896 from liboqs.
///
/// Classic McEliece is chosen because of its high security margin and its small
/// ciphertexts. The public keys are humongous, but (being static keys) the are never transmitted over
/// the wire so this is not a big problem.
pub struct SKEM;
/// # Safety
///
/// This Trait impl calls unsafe [oqs_sys] functions, that write to byte
/// slices only identified using raw pointers. It must be ensured that the raw
/// pointers point into byte slices of sufficient length, to avoid UB through
/// overwriting of arbitrary data. This is checked in the following code before
/// the unsafe calls, and an early return with an Err occurs if the byte slice
/// size does not match the required size.
///
/// __Note__: This requirement is stricter than necessary, it would suffice
/// to only check that the buffers are big enough, allowing them to be even
/// bigger. However, from a correctness point of view it does not make sense to
/// allow bigger buffers.
impl KEM for SKEM {
const SK_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_secret_key as usize;
const PK_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_public_key as usize;
const CT_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_ciphertext as usize;
const SHK_LEN: usize =
oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_shared_secret as usize;
fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError> {
RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?;
RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?;
unsafe {
oqs_sys::kem::OQS_KEM_classic_mceliece_460896_keypair(pk.as_mut_ptr(), sk.as_mut_ptr())
.to_rg_error()
}
}
fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError> {
RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?;
RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?;
RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?;
unsafe {
oqs_sys::kem::OQS_KEM_classic_mceliece_460896_encaps(
ct.as_mut_ptr(),
shk.as_mut_ptr(),
pk.as_ptr(),
)
.to_rg_error()
}
}
fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError> {
RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?;
RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?;
RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?;
unsafe {
oqs_sys::kem::OQS_KEM_classic_mceliece_460896_decaps(
shk.as_mut_ptr(),
ct.as_ptr(),
sk.as_ptr(),
)
.to_rg_error()
}
}
}
/// Implements a KEM that is secure against Chosen Plaintext Attacks (CPA).
/// In the context of rosenpass this is used for ephemeral keys.
/// Currently the implementation uses
/// [Kyber 512](https://openquantumsafe.org/liboqs/algorithms/kem/kyber) from liboqs.
///
/// This is being used for ephemeral keys; since these are use-once the first post quantum
/// wireguard paper claimed that CPA security would be sufficient. Nonetheless we choose kyber
/// which provides CCA security since there are no publicly vetted KEMs out there which provide
/// only CPA security.
pub struct EKEM;
/// # Safety
///
/// This Trait impl calls unsafe [oqs_sys] functions, that write to byte
/// slices only identified using raw pointers. It must be ensured that the raw
/// pointers point into byte slices of sufficient length, to avoid UB through
/// overwriting of arbitrary data. This is checked in the following code before
/// the unsafe calls, and an early return with an Err occurs if the byte slice
/// size does not match the required size.
///
/// __Note__: This requirement is stricter than necessary, it would suffice
/// to only check that the buffers are big enough, allowing them to be even
/// bigger. However, from a correctness point of view it does not make sense to
/// allow bigger buffers.
impl KEM for EKEM {
const SK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_secret_key as usize;
const PK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_public_key as usize;
const CT_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_ciphertext as usize;
const SHK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_shared_secret as usize;
fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError> {
RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?;
RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?;
unsafe {
oqs_sys::kem::OQS_KEM_kyber_512_keypair(pk.as_mut_ptr(), sk.as_mut_ptr())
.to_rg_error()
}
}
fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError> {
RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?;
RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?;
RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?;
unsafe {
oqs_sys::kem::OQS_KEM_kyber_512_encaps(
ct.as_mut_ptr(),
shk.as_mut_ptr(),
pk.as_ptr(),
)
.to_rg_error()
}
}
fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError> {
RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?;
RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?;
RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?;
unsafe {
oqs_sys::kem::OQS_KEM_kyber_512_decaps(
shk.as_mut_ptr(),
ct.as_ptr(),
sk.as_ptr(),
)
.to_rg_error()
}
}
}

107
rosenpass/src/prftree.rs Normal file
View File

@@ -0,0 +1,107 @@
use {
crate::{
coloring::Secret,
sodium::{hmac, hmac_into, KEY_SIZE},
},
anyhow::Result,
};
// TODO Use a proper Dec interface
#[derive(Clone, Debug)]
pub struct PrfTree([u8; KEY_SIZE]);
#[derive(Clone, Debug)]
pub struct PrfTreeBranch([u8; KEY_SIZE]);
#[derive(Clone, Debug)]
pub struct SecretPrfTree(Secret<KEY_SIZE>);
#[derive(Clone, Debug)]
pub struct SecretPrfTreeBranch(Secret<KEY_SIZE>);
impl PrfTree {
pub fn zero() -> Self {
Self([0u8; KEY_SIZE])
}
pub fn dup(self) -> PrfTreeBranch {
PrfTreeBranch(self.0)
}
pub fn into_secret_prf_tree(self) -> SecretPrfTree {
SecretPrfTree(Secret::from_slice(&self.0))
}
// TODO: Protocol! Use domain separation to ensure that
pub fn mix(self, v: &[u8]) -> Result<Self> {
Ok(Self(hmac(&self.0, v)?))
}
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretPrfTree> {
SecretPrfTree::prf_invoc(&self.0, v.secret())
}
pub fn into_value(self) -> [u8; KEY_SIZE] {
self.0
}
}
impl PrfTreeBranch {
pub fn mix(&self, v: &[u8]) -> Result<PrfTree> {
Ok(PrfTree(hmac(&self.0, v)?))
}
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretPrfTree> {
SecretPrfTree::prf_invoc(&self.0, v.secret())
}
}
impl SecretPrfTree {
pub fn prf_invoc(k: &[u8], d: &[u8]) -> Result<SecretPrfTree> {
let mut r = SecretPrfTree(Secret::zero());
hmac_into(r.0.secret_mut(), k, d)?;
Ok(r)
}
pub fn zero() -> Self {
Self(Secret::zero())
}
pub fn dup(self) -> SecretPrfTreeBranch {
SecretPrfTreeBranch(self.0)
}
pub fn danger_from_secret(k: Secret<KEY_SIZE>) -> Self {
Self(k)
}
pub fn mix(self, v: &[u8]) -> Result<SecretPrfTree> {
Self::prf_invoc(self.0.secret(), v)
}
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretPrfTree> {
Self::prf_invoc(self.0.secret(), v.secret())
}
pub fn into_secret(self) -> Secret<KEY_SIZE> {
self.0
}
pub fn into_secret_slice(mut self, v: &[u8], dst: &[u8]) -> Result<()> {
hmac_into(self.0.secret_mut(), v, dst)
}
}
impl SecretPrfTreeBranch {
pub fn mix(&self, v: &[u8]) -> Result<SecretPrfTree> {
SecretPrfTree::prf_invoc(self.0.secret(), v)
}
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretPrfTree> {
SecretPrfTree::prf_invoc(self.0.secret(), v.secret())
}
// TODO: This entire API is not very nice; we need this for biscuits, but
// it might be better to extract a special "biscuit"
// labeled subkey and reinitialize the chain with this
pub fn danger_into_secret(self) -> Secret<KEY_SIZE> {
self.0
}
}

File diff suppressed because it is too large Load Diff

283
rosenpass/src/sodium.rs Normal file
View File

@@ -0,0 +1,283 @@
use crate::util::*;
use anyhow::{ensure, Result};
use libsodium_sys as libsodium;
use log::trace;
use static_assertions::const_assert_eq;
use std::os::raw::{c_ulonglong, c_void};
use std::ptr::{null as nullptr, null_mut as nullptr_mut};
pub const AEAD_TAG_LEN: usize = libsodium::crypto_aead_chacha20poly1305_IETF_ABYTES as usize;
pub const AEAD_NONCE_LEN: usize = libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize;
pub const XAEAD_TAG_LEN: usize = libsodium::crypto_aead_xchacha20poly1305_ietf_ABYTES as usize;
pub const XAEAD_NONCE_LEN: usize = libsodium::crypto_aead_xchacha20poly1305_IETF_NPUBBYTES as usize;
pub const NONCE0: [u8; libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize] =
[0u8; libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize];
pub const NOTHING: [u8; 0] = [0u8; 0];
pub const KEY_SIZE: usize = 32;
pub const MAC_SIZE: usize = 16;
const_assert_eq!(
KEY_SIZE,
libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize
);
const_assert_eq!(KEY_SIZE, libsodium::crypto_generichash_BYTES as usize);
macro_rules! sodium_call {
($name:ident, $($args:expr),*) => { attempt!({
ensure!(unsafe{libsodium::$name($($args),*)} > -1,
"Error in libsodium's {}.", stringify!($name));
Ok(())
})};
($name:ident) => { sodium_call!($name, ) };
}
#[inline]
pub fn sodium_init() -> Result<()> {
trace!("initializing libsodium");
sodium_call!(sodium_init)
}
#[inline]
pub fn sodium_memcmp(a: &[u8], b: &[u8]) -> bool {
a.len() == b.len()
&& unsafe {
let r = libsodium::sodium_memcmp(
a.as_ptr() as *const c_void,
b.as_ptr() as *const c_void,
a.len(),
);
r == 0
}
}
#[inline]
pub fn sodium_bigint_cmp(a: &[u8], b: &[u8]) -> i32 {
assert!(a.len() == b.len());
unsafe { libsodium::sodium_compare(a.as_ptr(), b.as_ptr(), a.len()) }
}
#[inline]
pub fn sodium_bigint_inc(v: &mut [u8]) {
unsafe {
libsodium::sodium_increment(v.as_mut_ptr(), v.len());
}
}
#[inline]
pub fn rng(buf: &mut [u8]) {
unsafe { libsodium::randombytes_buf(buf.as_mut_ptr() as *mut c_void, buf.len()) };
}
#[inline]
pub fn zeroize(buf: &mut [u8]) {
unsafe { libsodium::sodium_memzero(buf.as_mut_ptr() as *mut c_void, buf.len()) };
}
#[inline]
pub fn aead_enc_into(
ciphertext: &mut [u8],
key: &[u8],
nonce: &[u8],
ad: &[u8],
plaintext: &[u8],
) -> Result<()> {
assert!(ciphertext.len() == plaintext.len() + AEAD_TAG_LEN);
assert!(key.len() == libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize);
assert!(nonce.len() == libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize);
let mut clen: u64 = 0;
sodium_call!(
crypto_aead_chacha20poly1305_ietf_encrypt,
ciphertext.as_mut_ptr(),
&mut clen,
plaintext.as_ptr(),
plaintext.len() as c_ulonglong,
ad.as_ptr(),
ad.len() as c_ulonglong,
nullptr(), // nsec is not used
nonce.as_ptr(),
key.as_ptr()
)?;
assert!(clen as usize == ciphertext.len());
Ok(())
}
#[inline]
pub fn aead_dec_into(
plaintext: &mut [u8],
key: &[u8],
nonce: &[u8],
ad: &[u8],
ciphertext: &[u8],
) -> Result<()> {
assert!(ciphertext.len() == plaintext.len() + AEAD_TAG_LEN);
assert!(key.len() == libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize);
assert!(nonce.len() == libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize);
let mut mlen: u64 = 0;
sodium_call!(
crypto_aead_chacha20poly1305_ietf_decrypt,
plaintext.as_mut_ptr(),
&mut mlen as *mut c_ulonglong,
nullptr_mut(), // nsec is not used
ciphertext.as_ptr(),
ciphertext.len() as c_ulonglong,
ad.as_ptr(),
ad.len() as c_ulonglong,
nonce.as_ptr(),
key.as_ptr()
)?;
assert!(mlen as usize == plaintext.len());
Ok(())
}
#[inline]
pub fn xaead_enc_into(
ciphertext: &mut [u8],
key: &[u8],
nonce: &[u8],
ad: &[u8],
plaintext: &[u8],
) -> Result<()> {
assert!(ciphertext.len() == plaintext.len() + XAEAD_NONCE_LEN + XAEAD_TAG_LEN);
assert!(key.len() == libsodium::crypto_aead_xchacha20poly1305_IETF_KEYBYTES as usize);
let (n, ct) = ciphertext.split_at_mut(XAEAD_NONCE_LEN);
n.copy_from_slice(nonce);
let mut clen: u64 = 0;
sodium_call!(
crypto_aead_xchacha20poly1305_ietf_encrypt,
ct.as_mut_ptr(),
&mut clen,
plaintext.as_ptr(),
plaintext.len() as c_ulonglong,
ad.as_ptr(),
ad.len() as c_ulonglong,
nullptr(), // nsec is not used
nonce.as_ptr(),
key.as_ptr()
)?;
assert!(clen as usize == ct.len());
Ok(())
}
#[inline]
pub fn xaead_dec_into(
plaintext: &mut [u8],
key: &[u8],
ad: &[u8],
ciphertext: &[u8],
) -> Result<()> {
assert!(ciphertext.len() == plaintext.len() + XAEAD_NONCE_LEN + XAEAD_TAG_LEN);
assert!(key.len() == libsodium::crypto_aead_xchacha20poly1305_IETF_KEYBYTES as usize);
let (n, ct) = ciphertext.split_at(XAEAD_NONCE_LEN);
let mut mlen: u64 = 0;
sodium_call!(
crypto_aead_xchacha20poly1305_ietf_decrypt,
plaintext.as_mut_ptr(),
&mut mlen as *mut c_ulonglong,
nullptr_mut(), // nsec is not used
ct.as_ptr(),
ct.len() as c_ulonglong,
ad.as_ptr(),
ad.len() as c_ulonglong,
n.as_ptr(),
key.as_ptr()
)?;
assert!(mlen as usize == plaintext.len());
Ok(())
}
#[inline]
fn blake2b_flexible(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> {
const KEY_MIN: usize = libsodium::crypto_generichash_KEYBYTES_MIN as usize;
const KEY_MAX: usize = libsodium::crypto_generichash_KEYBYTES_MAX as usize;
const OUT_MIN: usize = libsodium::crypto_generichash_BYTES_MIN as usize;
const OUT_MAX: usize = libsodium::crypto_generichash_BYTES_MAX as usize;
assert!(key.is_empty() || (KEY_MIN <= key.len() && key.len() <= KEY_MAX));
assert!(OUT_MIN <= out.len() && out.len() <= OUT_MAX);
let kptr = match key.len() {
// NULL key
0 => nullptr(),
_ => key.as_ptr(),
};
sodium_call!(
crypto_generichash_blake2b,
out.as_mut_ptr(),
out.len(),
data.as_ptr(),
data.len() as c_ulonglong,
kptr,
key.len()
)
}
// TODO: Use proper streaming hash; for mix_hash too.
#[inline]
pub fn hash_into(out: &mut [u8], data: &[u8]) -> Result<()> {
assert!(out.len() == KEY_SIZE);
blake2b_flexible(out, &NOTHING, data)
}
#[inline]
pub fn hash(data: &[u8]) -> Result<[u8; KEY_SIZE]> {
let mut r = [0u8; KEY_SIZE];
hash_into(&mut r, data)?;
Ok(r)
}
#[inline]
pub fn mac_into(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> {
assert!(out.len() == KEY_SIZE);
assert!(key.len() == KEY_SIZE);
blake2b_flexible(out, key, data)
}
#[inline]
pub fn mac(key: &[u8], data: &[u8]) -> Result<[u8; KEY_SIZE]> {
let mut r = [0u8; KEY_SIZE];
mac_into(&mut r, key, data)?;
Ok(r)
}
#[inline]
pub fn mac16(key: &[u8], data: &[u8]) -> Result<[u8; 16]> {
assert!(key.len() == KEY_SIZE);
let mut out = [0u8; 16];
blake2b_flexible(&mut out, key, data)?;
Ok(out)
}
#[inline]
pub fn hmac_into(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> {
// Not bothering with padding; the implementation
// uses appropriately sized keys.
ensure!(key.len() == KEY_SIZE);
const IPAD: [u8; KEY_SIZE] = [0x36u8; KEY_SIZE];
let mut temp_key = [0u8; KEY_SIZE];
temp_key.copy_from_slice(key);
xor_into(&mut temp_key, &IPAD);
let outer_data = mac(&temp_key, data)?;
const OPAD: [u8; KEY_SIZE] = [0x5Cu8; KEY_SIZE];
temp_key.copy_from_slice(key);
xor_into(&mut temp_key, &OPAD);
mac_into(out, &temp_key, &outer_data)
}
#[inline]
pub fn hmac(key: &[u8], data: &[u8]) -> Result<[u8; KEY_SIZE]> {
let mut r = [0u8; KEY_SIZE];
hmac_into(&mut r, key, data)?;
Ok(r)
}
// Choose a fully random u64
pub fn rand_u64() -> u64 {
let mut buf = [0u8; 8];
rng(&mut buf);
u64::from_le_bytes(buf)
}
// Choose a random f64 in [0; 1] inclusive; quick and dirty
pub fn rand_f64() -> f64 {
(rand_u64() as f64) / (u64::MAX as f64)
}

48
rosenpass/src/usage.md Normal file
View File

@@ -0,0 +1,48 @@
NAME
{0} Perform post-quantum secure key exchanges for wireguard and other services.
SYNOPSIS
{0} [ COMMAND ] [ OPTIONS ]... [ ARGS ]...
DESCRIPTION
{0} performs cryptographic key exchanges that are secure against quantum-computers and outputs the keys.
These keys can then be passed to various services such as wireguard or other vpn services
as pre-shared-keys to achieve security against attackers with quantum computers.
COMMANDS
keygen private-key <file-path> public-key <file-path>
Generate a keypair to use in the exchange command later. Send the public-key file to your communication partner
and keep the private-key file a secret!
exchange private-key <file-path> public-key <file-path> [ OPTIONS ]... PEER...\n"
Start a process to exchange keys with the specified peers. You should specify at least one peer.
OPTIONS
listen <ip>[:<port>]
Instructs {0} to listen on the specified interface and port. By default {0} will listen on all interfaces and select a random port.
verbose
Extra logging
PEER := peer public-key <file-path> [endpoint <ip>[:<port>]] [preshared-key <file-path>] [outfile <file-path>] [wireguard <dev> <peer> <extra_params>]
Instructs {0} to exchange keys with the given peer and write the resulting PSK into the given output file.
You must either specify the outfile or wireguard output option.
endpoint <ip>[:<port>]
Specifies the address where the peer can be reached. This will be automatically updated after the first successful
key exchange with the peer. If this is unspecified, the peer must initiate the connection.
preshared-key <file-path>
You may specify a pre-shared key which will be mixed into the final secret.
outfile <file-path>
You may specify a file to write the exchanged keys to. If this option is specified, {0} will
write a notification to standard out every time the key is updated.
wireguard <dev> <peer> <extra_params>
This allows you to directly specify a wireguard peer to deploy the pre-shared-key to.
You may specify extra parameters you would pass to `wg set` besides the preshared-key parameter which is used by {0}.
This makes it possible to add peers entirely from {0}.

123
rosenpass/src/util.rs Normal file
View File

@@ -0,0 +1,123 @@
use base64::{
display::Base64Display as B64Display, read::DecoderReader as B64Reader,
write::EncoderWriter as B64Writer,
};
use std::{
borrow::{Borrow, BorrowMut},
cmp::min,
io::{Read, Write},
time::{Duration, Instant},
};
#[inline]
pub fn xor_into(a: &mut [u8], b: &[u8]) {
assert!(a.len() == b.len());
for (av, bv) in a.iter_mut().zip(b.iter()) {
*av ^= *bv;
}
}
// TODO: Zeroize result?
/** Concatenate two byte arrays */
#[macro_export]
macro_rules! cat {
($len:expr; $($toks:expr),+) => {{
let mut buf = [0u8; $len];
let mut off = 0;
$({
let tok = $toks;
let tr = ::std::borrow::Borrow::<[u8]>::borrow(tok);
(&mut buf[off..(off + tr.len())]).copy_from_slice(tr);
off += tr.len();
})+
assert!(off == buf.len(), "Size mismatch in cat!()");
buf
}}
}
// TODO: consistent inout ordering
pub fn cpy<T: BorrowMut<[u8]> + ?Sized, F: Borrow<[u8]> + ?Sized>(src: &F, dst: &mut T) {
dst.borrow_mut().copy_from_slice(src.borrow());
}
pub fn cpy_min<T: BorrowMut<[u8]> + ?Sized, F: Borrow<[u8]> + ?Sized>(src: &F, to: &mut T) {
let src = src.borrow();
let dst = to.borrow_mut();
let len = min(src.len(), dst.len());
dst[..len].copy_from_slice(&src[..len]);
}
/// Try block basically…returns a result and allows the use of the question mark operator inside
#[macro_export]
macro_rules! attempt {
($block:expr) => {
(|| -> ::anyhow::Result<_> { $block })()
};
}
const B64TYPE: base64::Config = base64::STANDARD;
pub fn fmt_b64<'a>(payload: &'a [u8]) -> B64Display<'a> {
B64Display::<'a>::with_config(payload, B64TYPE)
}
pub fn b64_writer<W: Write>(w: W) -> B64Writer<W> {
B64Writer::new(w, B64TYPE)
}
pub fn b64_reader<R: Read>(r: &mut R) -> B64Reader<'_, R> {
B64Reader::new(r, B64TYPE)
}
// TODO remove this once std::cmp::max becomes const
pub const fn max_usize(a: usize, b: usize) -> usize {
if a > b {
a
} else {
b
}
}
#[derive(Clone, Debug)]
pub struct Timebase(Instant);
impl Default for Timebase {
fn default() -> Self {
Self(Instant::now())
}
}
impl Timebase {
pub fn now(&self) -> f64 {
self.0.elapsed().as_secs_f64()
}
pub fn dur(&self, t: f64) -> Duration {
Duration::from_secs_f64(t)
}
}
#[macro_export]
macro_rules! multimatch {
($val:expr) => {{ () }};
($val:expr, $($p:pat => $thn:expr),*) => {{
let v = $val;
($(if let $p = v { Some($thn) } else { None }),*)
}};
}
pub fn mutating<T, F>(mut v: T, f: F) -> T
where
F: Fn(&mut T),
{
f(&mut v);
v
}
pub fn sideeffect<T, F>(v: T, f: F) -> T
where
F: Fn(&T),
{
f(&v);
v
}

View File

@@ -8,21 +8,21 @@ fn generate_keys() {
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("keygen");
fs::create_dir_all(&tmpdir).unwrap();
let secret_key_path = tmpdir.join("secret-key");
let public_key_path = tmpdir.join("public-key");
let priv_key_path = tmpdir.join("private-key");
let pub_key_path = tmpdir.join("public-key");
let output = test_bin::get_test_bin(BIN)
.args(["gen-keys", "--secret-key"])
.arg(&secret_key_path)
.arg("--public-key")
.arg(&public_key_path)
.args(["keygen", "private-key"])
.arg(&priv_key_path)
.arg("public-key")
.arg(&pub_key_path)
.output()
.expect("Failed to start {BIN}");
assert_eq!(String::from_utf8_lossy(&output.stdout), "");
assert!(secret_key_path.is_file());
assert!(public_key_path.is_file());
assert!(priv_key_path.is_file());
assert!(pub_key_path.is_file());
// cleanup
fs::remove_dir_all(&tmpdir).unwrap();
@@ -30,8 +30,11 @@ fn generate_keys() {
fn find_udp_socket() -> u16 {
for port in 1025..=u16::MAX {
if UdpSocket::bind(("127.0.0.1", port)).is_ok() {
return port;
match UdpSocket::bind(("127.0.0.1", port)) {
Ok(_) => {
return port;
}
_ => {}
}
}
panic!("no free UDP port found");
@@ -43,22 +46,22 @@ fn check_exchange() {
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 priv_key_paths = [tmpdir.join("private-key-0"), tmpdir.join("private-key-1")];
let pub_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
for (secret_key_path, pub_key_path) in secret_key_paths.iter().zip(public_key_paths.iter()) {
for (priv_key_path, pub_key_path) in priv_key_paths.iter().zip(pub_key_paths.iter()) {
let output = test_bin::get_test_bin(BIN)
.args(["gen-keys", "--secret-key"])
.arg(secret_key_path)
.arg("--public-key")
.arg(pub_key_path)
.args(["keygen", "private-key"])
.arg(&priv_key_path)
.arg("public-key")
.arg(&pub_key_path)
.output()
.expect("Failed to start {BIN}");
assert_eq!(String::from_utf8_lossy(&output.stdout), "");
assert!(secret_key_path.is_file());
assert!(priv_key_path.is_file());
assert!(pub_key_path.is_file());
}
@@ -66,12 +69,12 @@ fn check_exchange() {
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])
.args(["exchange", "private-key"])
.arg(&priv_key_paths[0])
.arg("public-key")
.arg(&public_key_paths[0])
.arg(&pub_key_paths[0])
.args(["listen", &listen_addr, "verbose", "peer", "public-key"])
.arg(&public_key_paths[1])
.arg(&pub_key_paths[1])
.arg("outfile")
.arg(&shared_key_paths[0])
.stdout(Stdio::null())
@@ -79,16 +82,14 @@ fn check_exchange() {
.spawn()
.expect("Failed to start {BIN}");
std::thread::sleep(Duration::from_millis(500));
// start second process, the client
let mut client = test_bin::get_test_bin(BIN)
.args(["exchange", "secret-key"])
.arg(&secret_key_paths[1])
.args(["exchange", "private-key"])
.arg(&priv_key_paths[1])
.arg("public-key")
.arg(&public_key_paths[1])
.arg(&pub_key_paths[1])
.args(["verbose", "peer", "public-key"])
.arg(&public_key_paths[0])
.arg(&pub_key_paths[0])
.args(["endpoint", &listen_addr])
.arg("outfile")
.arg(&shared_key_paths[1])

391
rp
View File

@@ -1,391 +0,0 @@
#!/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 "$@"

13
rp/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "rp"
version = "0.1.0"
edition = "2021"
authors = ["wucke13 <wucke13@gmail.com>", "Karolin Varner <karo@cupdev.net>"]
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"
[dependencies]
clap = { version = "4.1.8", features = ["derive"] }

55
rp/src/main.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::path::PathBuf;
use clap::{Parser, Subcommand};
// Usage: ../rp-old [explain] [verbose] genkey|pubkey|exchange [ARGS]...
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Explain what is done
#[arg(short, long)]
explain: bool,
/// Be verbose about what's going on
#[arg(short, long)]
verbose: bool,
#[command(subcommand)]
command: Command,
}
/// Doc comment
#[derive(Subcommand, Debug)]
// #[command(PARENT CMD ATTRIBUTE)]
enum Command {
/// Generate a keypair
// --- Requirements ---
// requires wireguard
// should not exist before
// should be dir after
// should contain three files after pqpk, pqsk, wgsk
Genkey {
private_keys_dir: PathBuf,
},
/// Generate public keys
// --- Requirements ---
// requires wireguard
// requires private_keys_dir to exist
// should create public_keys_dir
// should copy pqpk from private_ to public_keys_dir
// should generate wgpk to public_keys_dir
Pubkey {
private_keys_dir: PathBuf,
public_keys_dir: PathBuf,
},
Exchange {},
}
fn main() {
let args = Cli::parse();
println!("{args:#?}");
}

View File

@@ -1,23 +0,0 @@
[package]
name = "rosenpass-secret-memory"
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 utilities for storing secrets in memory"
homepage = "https://rosenpass.eu/"
repository = "https://github.com/rosenpass/rosenpass"
readme = "readme.md"
[dependencies]
anyhow = { workspace = true }
rosenpass-to = { workspace = true }
rosenpass-util = { workspace = true }
zeroize = { workspace = true }
rand = { workspace = true }
memsec = { workspace = true }
allocator-api2 = { workspace = true }
log = { workspace = true }
[dev-dependencies]
allocator-api2-tests = { workspace = true }

View File

@@ -1,5 +0,0 @@
# Rosenpass secure memory library
Rosenpass internal library providing utilities for securely storing secret data in memory.
This is an internal library; not guarantee is made about its API at this point in time.

Some files were not shown because too many files have changed in this diff Show More