Compare commits

..

41 Commits

Author SHA1 Message Date
Karolin Varner
737781c8bc chore(coverage): Fix missing coverage from API integration tests
Three changes:
1. We neglected to forward stderr from Rosenpass subprocess two
   in the API setup integration test (driveby fix)
2. Added rudimentary signal handling for program termination
   to rosenpass, specifically for the coverage reporting
3. Apparently std::process::Child::kill() sends SIGKILL and not
   SIGTERM, so our nice new signal handler was never used.
   Switched to a rustix based child reaper.

(2) and (3) where necessary because llvm-cov does not produce coverage
when a subprocess terminates due to a default signal handler.
2024-12-11 00:01:44 +01:00
Karolin Varner
4ea1c76b81 Add documentation for the ciphers crate (#506) 2024-12-10 23:57:56 +01:00
David Niehues
a789f801ab fix formatting 2024-12-10 12:35:22 +01:00
David Niehues
be06f8adec add tests and documentation for hash_domain.rs 2024-12-10 12:35:22 +01:00
David Niehues
03d3c70e2e document lib.rs and mod.rs, and format documentation for incorrect_hmac_blake2b.rs 2024-12-10 12:35:22 +01:00
David Niehues
94ba99d89b add documentation for hash_domain.rs 2024-12-10 12:35:22 +01:00
David Niehues
667a994253 add documentation for blake2b hmac 2024-12-10 12:35:22 +01:00
David Niehues
9561ea4a47 add documentation for xchacha20polxy1305_ietf.rs and improve documentaion for other implementations for chacha20poly1305 2024-12-10 12:35:22 +01:00
David Niehues
fb641f8568 document chacha20poly1305 as implemented in RustCrypto 2024-12-10 12:35:22 +01:00
David Niehues
6e16956bc7 document chacha20poly1305 as implemented in libcrux 2024-12-10 12:35:22 +01:00
David Niehues
eeb738b649 add documentation and doc-tests for blake2b.rs 2024-12-10 12:35:21 +01:00
Karolin Varner
2d20ad6335 fix: CI issues under Darwin
Some checks failed
Nix / Build x86_64-linux.rp-static (push) Has been cancelled
Nix / Build x86_64-linux.whitepaper (push) Has been cancelled
Nix / Run Nix checks on x86_64-linux (push) Has been cancelled
Nix / Upload whitepaper x86_64-linux (push) Has been cancelled
QC / prettier (push) Has been cancelled
QC / Shellcheck (push) Has been cancelled
QC / Rust Format (push) Has been cancelled
QC / cargo-bench (push) Has been cancelled
QC / mandoc (push) Has been cancelled
QC / cargo-audit (push) Has been cancelled
QC / cargo-clippy (push) Has been cancelled
QC / cargo-doc (push) Has been cancelled
QC / cargo-test (macos-13) (push) Has been cancelled
QC / cargo-test (ubuntu-latest) (push) Has been cancelled
QC / cargo-test-nix-devshell-x86_64-linux (push) Has been cancelled
QC / cargo-fuzz (push) Has been cancelled
QC / codecov (push) Has been cancelled
Regressions / multi-peer (push) Has been cancelled
Regressions / boot-race (push) Has been cancelled
Nix / Build i686-linux.default (push) Has been cancelled
Nix / Build i686-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build x86_64-darwin.default (push) Has been cancelled
Nix / Build x86_64-darwin.release-package (push) Has been cancelled
Nix / Build x86_64-darwin.rosenpass-oci-image (push) Has been cancelled
Nix / Build x86_64-linux.default (push) Has been cancelled
Nix / Build x86_64-linux.proof-proverif (push) Has been cancelled
Nix / Build x86_64-linux.release-package (push) Has been cancelled
Nix / Build x86_64-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build aarch64-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build x86_64-linux.rosenpass-static-oci-image (push) Has been cancelled
2024-12-09 15:35:34 +01:00
Jacek Galowicz
df3d1821c8 Fix build for mac 2024-12-09 15:35:34 +01:00
Jacek Galowicz
6048ebd3d9 rp systemd unit file: introduce and test 2024-12-09 15:35:34 +01:00
Jacek Galowicz
cd7558594f rp: Add exchange-config command
This is similar to `rosenpass exchange`/`rosenpass exchange-config`.
It's however slightly different to the configuration file models the `rp
exchange` command line.
2024-12-09 15:35:34 +01:00
Jacek Galowicz
022cdc4ffa rp: set allowed-ips as routes
Prepare the rp app for a systemd unit file that sets up wireguard
connections.
2024-12-09 15:35:34 +01:00
Jacek Galowicz
06d4e289a5 rp: Add ip parameter to exchange command
Prepare the `rp` app for a systemd unit that sets up a wireguard connection.
2024-12-09 15:35:34 +01:00
Jacek Galowicz
f9dce3fc9a rosenpass systemd unit file: introduce and test 2024-12-09 15:35:34 +01:00
Karolin Varner
d9f3c8fb96 Documentation and unit tests for the rosenpass crate (#520)
Some checks are pending
Nix / Build x86_64-linux.default (push) Blocked by required conditions
Nix / Build x86_64-linux.proof-proverif (push) Blocked by required conditions
Nix / Build x86_64-linux.proverif-patched (push) Waiting to run
Nix / Build x86_64-linux.release-package (push) Blocked by required conditions
Nix / Build x86_64-linux.rosenpass (push) Waiting to run
Nix / Build aarch64-linux.rosenpass (push) Waiting to run
Nix / Build aarch64-linux.rp (push) Waiting to run
Nix / Build x86_64-linux.rosenpass-oci-image (push) Blocked by required conditions
Nix / Build aarch64-linux.rosenpass-oci-image (push) Blocked by required conditions
Nix / Build x86_64-linux.rosenpass-static (push) Waiting to run
Nix / Build x86_64-linux.rp-static (push) Waiting to run
Nix / Build x86_64-linux.rosenpass-static-oci-image (push) Blocked by required conditions
Nix / Build x86_64-linux.whitepaper (push) Waiting to run
Nix / Run Nix checks on x86_64-linux (push) Waiting to run
Nix / Upload whitepaper x86_64-linux (push) Waiting to run
QC / cargo-bench (push) Waiting to run
QC / mandoc (push) Waiting to run
QC / cargo-audit (push) Waiting to run
QC / Shellcheck (push) Waiting to run
QC / Rust Format (push) Waiting to run
QC / prettier (push) Waiting to run
QC / cargo-clippy (push) Waiting to run
QC / cargo-doc (push) Waiting to run
QC / cargo-test (macos-13) (push) Waiting to run
QC / cargo-test (ubuntu-latest) (push) Waiting to run
QC / cargo-test-nix-devshell-x86_64-linux (push) Waiting to run
QC / cargo-fuzz (push) Waiting to run
QC / codecov (push) Waiting to run
Regressions / multi-peer (push) Waiting to run
Regressions / boot-race (push) Waiting to run
2024-12-09 07:23:09 +01:00
Karolin Varner
0ea9f1061e chore(doc): rosenpass::api 2024-12-08 23:29:31 +01:00
Karolin Varner
737f0bc9f9 chore(tests): Man page generation integration tests 2024-12-08 23:26:06 +01:00
Karolin Varner
32ebd18107 chore(doc): Docu & tests for rosenpass/src/main.rs 2024-12-08 23:26:06 +01:00
Karolin Varner
f04cff6d57 chore: Remove unused error case InvalidApiMessage 2024-12-08 23:26:06 +01:00
Karolin Varner
2c1a0a7451 chore(doc): document rosenpass/src/lib.rs 2024-12-08 23:26:06 +01:00
Karolin Varner
74fdb44680 chore(doc): Move doc from protocol::protocol into protocol 2024-12-08 00:45:58 +01:00
Karolin Varner
c3adbb7cf3 chore(doc): Documentation for gen-ipc-msg-types binary
It is very basic, because this a developer tool we should refactor anyway.
2024-12-08 00:45:58 +01:00
Karolin Varner
fa583ec6ae chore(doc): Document rosenpass:hash_domains 2024-12-07 17:32:42 +01:00
Karolin Varner
aa76db1e1c chore(rosenpass::msgs): Remove unused fields 2024-12-07 15:26:47 +01:00
Karolin Varner
c5699b5259 chore(doc): Documentation & tests for rosenpass::msgs 2024-12-07 15:26:47 +01:00
Karolin Varner
d3c52fdf64 chore(coverage): Use CodeCov token 2024-12-07 15:26:47 +01:00
Karolin Varner
b18f05ae19 feat(doc): How to format rust code 2024-12-07 15:26:47 +01:00
Karolin Varner
d8839ba341 feat(coverage): Reduce coverage false-negatives using grcov
Previously, we would report some tag style macros such as
`#[repr(packed)]` as being uncovered.

We are now also including doctests in our coverage reports.

Finally, a new script `coverage_report.sh` makes coverage checking
easier.
2024-12-07 15:26:47 +01:00
Karolin Varner
7022a93378 feat(docs): CONTRIBUTING.md with basic info about testing coverage 2024-12-07 15:26:47 +01:00
Karolin Varner
c9da9b8591 Hash based retransmission (#513)
Some checks failed
Nix / Build x86_64-linux.rosenpass-static (push) Has been cancelled
Nix / Build x86_64-linux.rp-static (push) Has been cancelled
Nix / Build x86_64-linux.whitepaper (push) Has been cancelled
Nix / Run Nix checks on x86_64-linux (push) Has been cancelled
Nix / Upload whitepaper x86_64-linux (push) Has been cancelled
QC / prettier (push) Has been cancelled
QC / Shellcheck (push) Has been cancelled
QC / Rust Format (push) Has been cancelled
QC / cargo-bench (push) Has been cancelled
QC / mandoc (push) Has been cancelled
QC / cargo-audit (push) Has been cancelled
QC / cargo-clippy (push) Has been cancelled
QC / cargo-doc (push) Has been cancelled
QC / cargo-test (macos-13) (push) Has been cancelled
QC / cargo-test (ubuntu-latest) (push) Has been cancelled
QC / cargo-test-nix-devshell-x86_64-linux (push) Has been cancelled
QC / cargo-fuzz (push) Has been cancelled
QC / codecov (push) Has been cancelled
Regressions / boot-race (push) Has been cancelled
Nix / Build i686-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build i686-linux.default (push) Has been cancelled
Nix / Build x86_64-darwin.default (push) Has been cancelled
Nix / Build x86_64-darwin.release-package (push) Has been cancelled
Nix / Build x86_64-darwin.rosenpass-oci-image (push) Has been cancelled
Nix / Build x86_64-linux.default (push) Has been cancelled
Nix / Build x86_64-linux.proof-proverif (push) Has been cancelled
Nix / Build x86_64-linux.release-package (push) Has been cancelled
Nix / Build x86_64-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build aarch64-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build x86_64-linux.rosenpass-static-oci-image (push) Has been cancelled
2024-12-07 12:37:01 +01:00
Karolin Varner
b483612cb7 feat(protocol): Hash-based retransmission mechanism
See the updated whitepaper for details.

Fixes: #331
2024-12-07 12:36:40 +01:00
Karolin Varner
a30805f8a0 feat(nix): Dev shell environment with full rust installation
$ nix develop .#fullEnv

This environment contains extra utilities such as the rust language
server.
2024-12-07 12:36:40 +01:00
Karolin Varner
a9b0a90ab5 chore: Update affiliations in whitepaper 2024-12-07 12:36:40 +01:00
Karolin Varner
2bc138e614 chore: Add more info on denial of service mitigation to whitepaper changelog 2024-12-07 12:36:40 +01:00
Karolin Varner
f97781039f build(deps): bump clap from 4.5.22 to 4.5.23 (#519)
Some checks are pending
Nix / Build x86_64-linux.default (push) Blocked by required conditions
Nix / Build x86_64-linux.proof-proverif (push) Blocked by required conditions
Nix / Build x86_64-linux.proverif-patched (push) Waiting to run
Nix / Build x86_64-linux.release-package (push) Blocked by required conditions
Nix / Build x86_64-linux.rosenpass (push) Waiting to run
Nix / Build aarch64-linux.rosenpass (push) Waiting to run
Nix / Build aarch64-linux.rp (push) Waiting to run
Nix / Build x86_64-linux.rosenpass-oci-image (push) Blocked by required conditions
Nix / Build aarch64-linux.rosenpass-oci-image (push) Blocked by required conditions
Nix / Build x86_64-linux.rosenpass-static (push) Waiting to run
Nix / Build x86_64-linux.rp-static (push) Waiting to run
Nix / Build x86_64-linux.rosenpass-static-oci-image (push) Blocked by required conditions
Nix / Build x86_64-linux.whitepaper (push) Waiting to run
Nix / Run Nix checks on x86_64-linux (push) Waiting to run
Nix / Upload whitepaper x86_64-linux (push) Waiting to run
QC / cargo-doc (push) Waiting to run
QC / cargo-bench (push) Waiting to run
QC / mandoc (push) Waiting to run
QC / cargo-audit (push) Waiting to run
QC / cargo-clippy (push) Waiting to run
QC / prettier (push) Waiting to run
QC / Shellcheck (push) Waiting to run
QC / Rust Format (push) Waiting to run
QC / cargo-test (macos-13) (push) Waiting to run
QC / cargo-test (ubuntu-latest) (push) Waiting to run
QC / cargo-test-nix-devshell-x86_64-linux (push) Waiting to run
QC / cargo-fuzz (push) Waiting to run
QC / codecov (push) Waiting to run
Regressions / multi-peer (push) Waiting to run
Regressions / boot-race (push) Waiting to run
2024-12-06 17:00:00 +01:00
dependabot[bot]
5eda161cf2 build(deps): bump clap from 4.5.22 to 4.5.23
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.22 to 4.5.23.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.22...clap_complete-v4.5.23)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-05 23:53:58 +00:00
Karolin Varner
a473fe6d9b build(deps): bump clap from 4.5.21 to 4.5.22 (#518)
Some checks failed
Nix / Build aarch64-linux.rosenpass (push) Has been cancelled
Nix / Build aarch64-linux.rp (push) Has been cancelled
Nix / Build x86_64-linux.rosenpass-static (push) Has been cancelled
Nix / Build x86_64-linux.rp-static (push) Has been cancelled
Nix / Upload whitepaper x86_64-linux (push) Has been cancelled
QC / prettier (push) Has been cancelled
QC / Shellcheck (push) Has been cancelled
QC / Rust Format (push) Has been cancelled
QC / cargo-bench (push) Has been cancelled
QC / mandoc (push) Has been cancelled
QC / cargo-audit (push) Has been cancelled
QC / cargo-clippy (push) Has been cancelled
QC / cargo-doc (push) Has been cancelled
QC / cargo-test (macos-13) (push) Has been cancelled
QC / cargo-test-nix-devshell-x86_64-linux (push) Has been cancelled
QC / cargo-fuzz (push) Has been cancelled
QC / codecov (push) Has been cancelled
Regressions / multi-peer (push) Has been cancelled
Regressions / boot-race (push) Has been cancelled
Nix / Build i686-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build x86_64-darwin.default (push) Has been cancelled
Nix / Build x86_64-darwin.release-package (push) Has been cancelled
Nix / Build x86_64-darwin.rosenpass-oci-image (push) Has been cancelled
Nix / Build x86_64-linux.default (push) Has been cancelled
Nix / Build x86_64-linux.proof-proverif (push) Has been cancelled
Nix / Build x86_64-linux.release-package (push) Has been cancelled
Nix / Build i686-linux.default (push) Has been cancelled
Nix / Build x86_64-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build aarch64-linux.rosenpass-oci-image (push) Has been cancelled
Nix / Build x86_64-linux.rosenpass-static-oci-image (push) Has been cancelled
2024-12-04 11:40:33 +01:00
32 changed files with 1905 additions and 249 deletions

View File

@@ -194,19 +194,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup default nightly
- run: rustup component add llvm-tools-preview
- run: |
cargo install cargo-llvm-cov || true
cargo llvm-cov \
--workspace\
--all-features \
--lcov \
--output-path coverage.lcov
cargo install grcov || true
./coverage_report.sh
# If using tarapulin
#- run: cargo install cargo-tarpaulin
#- run: cargo tarpaulin --out Xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage.lcov
files: ./target/grcov/lcov
verbose: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -1,38 +1,41 @@
**Making a new Release of Rosenpass — Cooking Recipe**
# Contributing to Rosenpass
If you have to change a file, do what it takes to get the change as commit on the main branch, then **start from step 0**.
If any other issue occurs
## Common operations
0. Make sure you are in the root directory of the project
- `cd "$(git rev-parse --show-toplevel)"`
1. Make sure you locally checked out the head of the main branch
- `git stash --include-untracked && git checkout main && git pull`
2. Make sure all tests pass
- `cargo test --workspace --all-features`
3. Make sure the current version in `rosenpass/Cargo.toml` matches that in the [last release on GitHub](https://github.com/rosenpass/rosenpass/releases)
- Only normal releases count, release candidates and draft releases can be ignored
4. Pick the kind of release that you want to make (`major`, `minor`, `patch`, `rc`, ...)
- See `cargo release --help` for more information on the available release types
- Pick `rc` if in doubt
5. Try to release a new version
- `cargo release rc --package rosenpass`
- An issue was reported? Go fix it, start again with step 0!
6. Actually make the release
- `cargo release rc --package rosenpass --execute`
- Tentatively wait for any interactions, such as entering ssh keys etc.
- You may be asked for your ssh key multiple times!
### Apply code formatting
**Frequently Asked Questions (FAQ)**
Format rust code:
- You have untracked files, which `cargo release` complains about?
- `git stash --include-untracked`
- You cannot push to crates.io because you are not logged in?
- Follow the steps displayed in [`cargo login`](https://doc.rust-lang.org/cargo/commands/cargo-login.html)
- How is the release page added to [GitHub Releases](https://github.com/rosenpass/rosenpass/releases) itself?
- Our CI Pipeline will create the release, once `cargo release` pushed the new version tag to the repo. The new release should pop up almost immediately in [GitHub Releases](https://github.com/rosenpass/rosenpass/releases) after the [Actions/Release](https://github.com/rosenpass/rosenpass/actions/workflows/release.yaml) pipeline started.
- No new release pops up in the `Release` sidebar element on the [main page](https://github.com/rosenpass/rosenpass)
- Did you push a `rc` release? This view only shows non-draft release, but `rc` releases are considered as draft. See [Releases](https://github.com/rosenpass/rosenpass/releases) page to see all (including draft!) releases.
- The release page was created on GitHub, but there are no assets/artifacts other than the source code tar ball/zip?
- The artifacts are generated and pushed automatically to the release, but this takes some time (a couple of minutes). You can check the respective CI pipeline: [Actions/Release](https://github.com/rosenpass/rosenpass/actions/workflows/release.yaml), which should start immediately after `cargo release` pushed the new release tag to the repo. The release artifacts only are added later to the release, once all jobs in bespoke pipeline finished.
- How are the release artifacts generated, and what are they?
- The release artifacts are built using one Nix derivation per platform, `nix build .#release-package`. It contains both statically linked versions of `rosenpass` itself and OCI container images.
```bash
cargo fmt
```
Format rust code in markdown files:
```bash
./format_rust_code.sh --mode fix
```
### Spawn a development environment with nix
```bash
nix develop .#fullEnv
```
You need to [install this nix package manager](https://wiki.archlinux.org/title/Nix) first.
### Run our test
Make sure to increase the stack size available; some of our cryptography operations require a lot of stack memory.
```bash
RUST_MIN_STACK=8388608 cargo test --workspace --all-features
```
### Generate coverage reports
Keep in mind that many of Rosenpass' tests are doctests, so to get an accurate read on our code coverage, you have to include doctests:
```bash
./coverage_report.sh
```

33
Cargo.lock generated
View File

@@ -381,9 +381,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.22"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
dependencies = [
"clap_builder",
"clap_derive",
@@ -391,13 +391,13 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.22"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
dependencies = [
"anstream",
"anstyle",
"clap_lex 0.7.2",
"clap_lex 0.7.4",
"strsim 0.11.1",
]
@@ -407,7 +407,7 @@ version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01"
dependencies = [
"clap 4.5.22",
"clap 4.5.23",
]
[[package]]
@@ -433,9 +433,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.2"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clap_mangen"
@@ -443,7 +443,7 @@ version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf"
dependencies = [
"clap 4.5.22",
"clap 4.5.23",
"roff",
]
@@ -1823,7 +1823,7 @@ name = "rosenpass"
version = "0.3.0-dev"
dependencies = [
"anyhow",
"clap 4.5.22",
"clap 4.5.23",
"clap_complete",
"clap_mangen",
"command-fds",
@@ -1850,6 +1850,7 @@ dependencies = [
"rustix",
"serde",
"serial_test",
"signal-hook",
"stacker",
"static_assertions",
"tempfile",
@@ -1964,7 +1965,7 @@ name = "rosenpass-wireguard-broker"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 4.5.22",
"clap 4.5.23",
"derive_builder 0.20.2",
"env_logger",
"libc",
@@ -2178,6 +2179,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"

View File

@@ -47,7 +47,7 @@ memsec = { git = "https://github.com/rosenpass/memsec.git", rev = "aceb9baee8aec
rand = "0.8.5"
typenum = "1.17.0"
log = { version = "0.4.22" }
clap = { version = "4.5.22", features = ["derive"] }
clap = { version = "4.5.23", features = ["derive"] }
clap_mangen = "0.2.24"
clap_complete = "4.5.38"
serde = { version = "1.0.215", features = ["derive"] }
@@ -74,6 +74,7 @@ hex = { version = "0.4.3" }
heck = { version = "0.5.0" }
libc = { version = "0.2" }
uds = { git = "https://github.com/rosenpass/uds" }
signal-hook = "0.3.17"
#Dev dependencies
serial_test = "3.2.0"
@@ -89,4 +90,4 @@ procspawn = { version = "1.0.1", features = ["test-support"] }
#Broker dependencies (might need cleanup or changes)
wireguard-uapi = { version = "3.0.0", features = ["xplatform"] }
command-fds = "0.2.3"
rustix = { version = "0.38.41", features = ["net", "fs"] }
rustix = { version = "0.38.41", features = ["net", "fs", "process"] }

View File

@@ -2,100 +2,196 @@ use anyhow::Result;
use rosenpass_secret_memory::Secret;
use rosenpass_to::To;
use crate::subtle::incorrect_hmac_blake2b as hash;
use crate::keyed_hash as hash;
pub use hash::KEY_LEN;
///
///```rust
/// # use rosenpass_ciphers::hash_domain::{HashDomain, HashDomainNamespace, SecretHashDomain, SecretHashDomainNamespace};
/// use rosenpass_secret_memory::Secret;
/// # rosenpass_secret_memory::secret_policy_use_only_malloc_secrets();
///
/// const PROTOCOL_IDENTIFIER: &str = "MY_PROTOCOL:IDENTIFIER";
/// # fn do_doc_test() -> Result<(), Box<dyn std::error::Error>> {
/// // create use once hash domain for the protocol identifier
/// let mut hash_domain = HashDomain::zero();
/// hash_domain = hash_domain.mix(PROTOCOL_IDENTIFIER.as_bytes())?;
/// // upgrade to reusable hash domain
/// let hash_domain_namespace: HashDomainNamespace = hash_domain.dup();
/// // derive new key
/// let key_identifier = "my_key_identifier";
/// let key = hash_domain_namespace.mix(key_identifier.as_bytes())?.into_value();
/// // derive a new key based on a secret
/// const MY_SECRET_LEN: usize = 21;
/// let my_secret_bytes = "my super duper secret".as_bytes();
/// let my_secret: Secret<21> = Secret::from_slice("my super duper secret".as_bytes());
/// let secret_hash_domain: SecretHashDomain = hash_domain_namespace.mix_secret(my_secret)?;
/// // derive a new key based on the secret key
/// let new_key_identifier = "my_new_key_identifier".as_bytes();
/// let new_key = secret_hash_domain.mix(new_key_identifier)?.into_secret();
///
/// # Ok(())
/// # }
/// # do_doc_test().unwrap();
///
///```
///
// TODO Use a proper Dec interface
/// A use-once hash domain for a specified key that can be used directly.
/// The key must consist of [KEY_LEN] many bytes. If the key must remain secret,
/// use [SecretHashDomain] instead.
#[derive(Clone, Debug)]
pub struct HashDomain([u8; KEY_LEN]);
/// A reusable hash domain for a namespace identified by the key.
/// The key must consist of [KEY_LEN] many bytes. If the key must remain secret,
/// use [SecretHashDomainNamespace] instead.
#[derive(Clone, Debug)]
pub struct HashDomainNamespace([u8; KEY_LEN]);
/// A use-once hash domain for a specified key that can be used directly
/// by wrapping it in [Secret]. The key must consist of [KEY_LEN] many bytes.
#[derive(Clone, Debug)]
pub struct SecretHashDomain(Secret<KEY_LEN>);
/// A reusable secure hash domain for a namespace identified by the key and that keeps the key secure
/// by wrapping it in [Secret]. The key must consist of [KEY_LEN] many bytes.
#[derive(Clone, Debug)]
pub struct SecretHashDomainNamespace(Secret<KEY_LEN>);
impl HashDomain {
/// Creates a nw [HashDomain] initialized with a all-zeros key.
pub fn zero() -> Self {
Self([0u8; KEY_LEN])
}
/// Turns this [HashDomain] into a [HashDomainNamespace], keeping the key.
pub fn dup(self) -> HashDomainNamespace {
HashDomainNamespace(self.0)
}
/// Turns this [HashDomain] into a [SecretHashDomain] by wrapping the key into a [Secret]
/// and creating a new [SecretHashDomain] from it.
pub fn turn_secret(self) -> SecretHashDomain {
SecretHashDomain(Secret::from_slice(&self.0))
}
// TODO: Protocol! Use domain separation to ensure that
/// Creates a new [HashDomain] by mixing in a new key `v`. Specifically,
/// it evaluates [hash::hash] with this HashDomain's key as the key and `v`
/// as the `data` and uses the result as the key for the new [HashDomain].
///
pub fn mix(self, v: &[u8]) -> Result<Self> {
Ok(Self(hash::hash(&self.0, v).collect::<[u8; KEY_LEN]>()?))
}
/// Creates a new [SecretHashDomain] by mixing in a new key `v`
/// by calling [SecretHashDomain::invoke_primitive] with this
/// [HashDomain]'s key as `k` and `v` as `d`.
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretHashDomain> {
SecretHashDomain::invoke_primitive(&self.0, v.secret())
}
/// Gets the key of this [HashDomain].
pub fn into_value(self) -> [u8; KEY_LEN] {
self.0
}
}
impl HashDomainNamespace {
/// Creates a new [HashDomain] by mixing in a new key `v`. Specifically,
/// it evaluates [hash::hash] with the key of this HashDomainNamespace key as the key and `v`
/// as the `data` and uses the result as the key for the new [HashDomain].
pub fn mix(&self, v: &[u8]) -> Result<HashDomain> {
Ok(HashDomain(
hash::hash(&self.0, v).collect::<[u8; KEY_LEN]>()?,
))
}
/// Creates a new [SecretHashDomain] by mixing in a new key `v`
/// by calling [SecretHashDomain::invoke_primitive] with the key of this
/// [HashDomainNamespace] as `k` and `v` as `d`.
///
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretHashDomain> {
SecretHashDomain::invoke_primitive(&self.0, v.secret())
}
}
impl SecretHashDomain {
/// Create a new [SecretHashDomain] with the given key `k` and data `d` by calling
/// [hash::hash] with `k` as the `key` and `d` s the `data`, and using the result
/// as the content for the new [SecretHashDomain].
/// Both `k` and `d` have to be exactly [KEY_LEN] bytes in length.
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)
}
/// Creates a new [SecretHashDomain] that is initialized with an all zeros key.
pub fn zero() -> Self {
Self(Secret::zero())
}
/// Turns this [SecretHashDomain] into a [SecretHashDomainNamespace].
pub fn dup(self) -> SecretHashDomainNamespace {
SecretHashDomainNamespace(self.0)
}
/// Creates a new [SecretHashDomain] from a [Secret] `k`.
///
/// It requires that `k` consist of exactly [KEY_LEN] bytes.
pub fn danger_from_secret(k: Secret<KEY_LEN>) -> Self {
Self(k)
}
/// Creates a new [SecretHashDomain] by mixing in a new key `v`. Specifically,
/// it evaluates [hash::hash] with this [SecretHashDomain]'s key as the key and `v`
/// as the `data` and uses the result as the key for the new [SecretHashDomain].
///
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
pub fn mix(self, v: &[u8]) -> Result<SecretHashDomain> {
Self::invoke_primitive(self.0.secret(), v)
}
/// Creates a new [SecretHashDomain] by mixing in a new key `v`
/// by calling [SecretHashDomain::invoke_primitive] with the key of this
/// [HashDomainNamespace] as `k` and `v` as `d`.
///
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretHashDomain> {
Self::invoke_primitive(self.0.secret(), v.secret())
}
/// Get the secret key data from this [SecretHashDomain].
pub fn into_secret(self) -> Secret<KEY_LEN> {
self.0
}
/// Evaluate [hash::hash] with this [SecretHashDomain]'s data as the `key` and
/// `dst` as the `data` and stores the result as the new data for this [SecretHashDomain].
///
/// It requires that both `v` and `d` consist of exactly [KEY_LEN] many bytes.
pub fn into_secret_slice(mut self, v: &[u8], dst: &[u8]) -> Result<()> {
hash::hash(v, dst).to(self.0.secret_mut())
}
}
impl SecretHashDomainNamespace {
/// Creates a new [SecretHashDomain] by mixing in a new key `v`. Specifically,
/// it evaluates [hash::hash] with the key of this HashDomainNamespace key as the key and `v`
/// as the `data` and uses the result as the key for the new [HashDomain].
///
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
pub fn mix(&self, v: &[u8]) -> Result<SecretHashDomain> {
SecretHashDomain::invoke_primitive(self.0.secret(), v)
}
/// Creates a new [SecretHashDomain] by mixing in a new key `v`
/// by calling [SecretHashDomain::invoke_primitive] with the key of this
/// [HashDomainNamespace] as `k` and `v` as `d`.
///
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretHashDomain> {
SecretHashDomain::invoke_primitive(self.0.secret(), v.secret())
}
@@ -103,6 +199,7 @@ impl SecretHashDomainNamespace {
// 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
/// Get the secret key data from this [SecretHashDomain].
pub fn danger_into_secret(self) -> Secret<KEY_LEN> {
self.0
}

View File

@@ -2,12 +2,25 @@ use static_assertions::const_assert;
pub mod subtle;
/// All keyed primitives in this crate use 32 byte keys
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);
/// Keyed hashing
///
/// This should only be used for implementation details; anything with relevance
/// to the cryptographic protocol should use the facilities in [hash_domain], (though
/// hash domain uses this module internally)
pub mod keyed_hash {
pub use crate::subtle::incorrect_hmac_blake2b::{
hash, KEY_LEN, KEY_MAX, KEY_MIN, OUT_MAX, OUT_MIN,
};
}
/// Authenticated encryption with associated data
/// Chacha20poly1305 is used.
pub mod aead {
#[cfg(not(feature = "experiment_libcrux"))]
pub use crate::subtle::chacha20poly1305_ietf::{decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN};
@@ -18,6 +31,7 @@ pub mod aead {
}
/// Authenticated encryption with associated data with a constant nonce
/// XChacha20poly1305 is used.
pub mod xaead {
pub use crate::subtle::xchacha20poly1305_ietf::{
decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN,
@@ -26,6 +40,12 @@ pub mod xaead {
pub mod hash_domain;
/// This crate includes two key encapsulation mechanisms.
/// Namely ClassicMceliece460896 (as [StaticKem]) and Kyber512 (as [EphemeralKem]).
///
/// See [rosenpass_oqs::ClassicMceliece460896](rosenpass_oqs::ClassicMceliece460896)
/// and [rosenpass_oqs::Kyber512](rosenpass_oqs::Kyber512) for more details on the specific KEMS.
///
pub mod kem {
pub use rosenpass_oqs::ClassicMceliece460896 as StaticKem;
pub use rosenpass_oqs::Kyber512 as EphemeralKem;

View File

@@ -9,19 +9,43 @@ use blake2::Blake2bMac;
use rosenpass_to::{ops::copy_slice, with_destination, To};
use rosenpass_util::typenum2const;
/// Specify that the used implementation of BLAKE2b is the MAC version of BLAKE2b
/// with output and key length of 32 bytes (see [Blake2bMac<U32>]).
type Impl = Blake2bMac<U32>;
type KeyLen = <Impl as KeySizeUser>::KeySize;
type OutLen = <Impl as OutputSizeUser>::OutputSize;
/// The key length for BLAKE2b supported by this API. Currently 32 Bytes.
const KEY_LEN: usize = typenum2const! { KeyLen };
/// The output length for BLAKE2b supported by this API. Currently 32 Bytes.
const OUT_LEN: usize = typenum2const! { OutLen };
/// Minimal key length supported by this API (identical to [KEY_LEN])
pub const KEY_MIN: usize = KEY_LEN;
/// maximal key length supported by this API (identical to [KEY_LEN])
pub const KEY_MAX: usize = KEY_LEN;
/// minimal output length supported by this API (identical [OUT_LEN])
pub const OUT_MIN: usize = OUT_LEN;
/// maximal output length supported by this API (identical [OUT_LEN])
pub const OUT_MAX: usize = OUT_LEN;
/// Hashes the given `data` with the [Blake2bMac<U32>] hash function under the given `key`.
/// The [KEY_LEN] and [OUT_LEN] are both set to 32 bytes (or 256 bits).
///
/// # Examples
///
///```rust
/// # use rosenpass_ciphers::subtle::blake2b::hash;
/// use rosenpass_to::To;
/// let zero_key: [u8; 32] = [0; 32];
/// let data: [u8; 32] = [255; 32];
/// // buffer for the hash output
/// let mut hash_data: [u8; 32] = [0u8; 32];
///
/// assert!(hash(&zero_key, &data).to(&mut hash_data).is_ok(), "Hashing has to return OK result");
///```
///
#[inline]
pub fn hash<'a>(key: &'a [u8], data: &'a [u8]) -> impl To<[u8], anyhow::Result<()>> + 'a {
with_destination(|out: &mut [u8]| {
@@ -36,7 +60,6 @@ pub fn hash<'a>(key: &'a [u8], data: &'a [u8]) -> impl To<[u8], anyhow::Result<(
let tmp = GenericArray::from_mut_slice(tmp.as_mut());
h.finalize_into(tmp);
copy_slice(tmp.as_ref()).to(out);
Ok(())
})
}

View File

@@ -6,10 +6,39 @@ use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::ChaCha20Poly1305 as AeadImpl;
use chacha20poly1305::{AeadCore, AeadInPlace, KeyInit, KeySizeUser};
/// The key length is 32 bytes or 256 bits.
pub const KEY_LEN: usize = typenum2const! { <AeadImpl as KeySizeUser>::KeySize };
/// The MAC tag length is 16 bytes or 128 bits.
pub const TAG_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::TagSize };
/// The nonce length is 12 bytes or 96 bits.
pub const NONCE_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::NonceSize };
/// Encrypts using ChaCha20Poly1305 as implemented in [RustCrypto](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305).
/// `key` MUST be chosen (pseudo-)randomly and `nonce` MOST NOT be reused. The `key` slice MUST have
/// a length of [KEY_LEN]. The `nonce` slice MUST have a length of [NONCE_LEN]. The last [TAG_LEN] bytes
/// written in `ciphertext` are the tag guaranteeing integrity. `ciphertext` MUST have a capacity of
/// `plaintext.len()` + [TAG_LEN].
///
/// # Examples
///```rust
/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf::{encrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
///
/// const PLAINTEXT_LEN: usize = 43;
/// let plaintext = "post-quantum cryptography is very important".as_bytes();
/// assert_eq!(PLAINTEXT_LEN, plaintext.len());
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
/// let mut ciphertext_buffer = [0u8;PLAINTEXT_LEN + TAG_LEN];
///
/// let res: anyhow::Result<()> = encrypt(&mut ciphertext_buffer, key, nonce, additional_data, plaintext);
/// assert!(res.is_ok());
/// # let expected_ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17,
/// # 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80,
/// # 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191,
/// # 8, 114, 85, 4, 25];
/// # assert_eq!(expected_ciphertext, &ciphertext_buffer);
///```
#[inline]
pub fn encrypt(
ciphertext: &mut [u8],
@@ -26,6 +55,33 @@ pub fn encrypt(
Ok(())
}
/// Decrypts a `ciphertext` and verifies the integrity of the `ciphertext` and the additional data
/// `ad`. using ChaCha20Poly1305 as implemented in [RustCrypto](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305).
///
/// The `key` slice MUST have a length of [KEY_LEN]. The `nonce` slice MUST have a length of
/// [NONCE_LEN]. The plaintext buffer must have a capacity of `ciphertext.len()` - [TAG_LEN].
///
/// # Examples
///```rust
/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf::{decrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
/// let ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17,
/// 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80,
/// 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191,
/// 8, 114, 85, 4, 25]; // this is the ciphertext generated by the example for the encryption
/// const PLAINTEXT_LEN: usize = 43;
/// assert_eq!(PLAINTEXT_LEN + TAG_LEN, ciphertext.len());
///
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
/// let mut plaintext_buffer = [0u8; PLAINTEXT_LEN];
///
/// let res: anyhow::Result<()> = decrypt(&mut plaintext_buffer, key, nonce, additional_data, ciphertext);
/// assert!(res.is_ok());
/// let expected_plaintext = "post-quantum cryptography is very important".as_bytes();
/// assert_eq!(expected_plaintext, plaintext_buffer);
///
///```
#[inline]
pub fn decrypt(
plaintext: &mut [u8],

View File

@@ -3,10 +3,40 @@ use rosenpass_to::To;
use zeroize::Zeroize;
/// The key length is 32 bytes or 256 bits.
pub const KEY_LEN: usize = 32; // Grrrr! Libcrux, please provide me these constants.
/// The MAC tag length is 16 bytes or 128 bits.
pub const TAG_LEN: usize = 16;
/// The nonce length is 12 bytes or 96 bits.
pub const NONCE_LEN: usize = 12;
/// Encrypts using ChaCha20Poly1305 as implemented in [libcrux](https://github.com/cryspen/libcrux).
/// Key and nonce MUST be chosen (pseudo-)randomly. The `key` slice MUST have a length of
/// [KEY_LEN]. The `nonce` slice MUST have a length of [NONCE_LEN]. The last [TAG_LEN] bytes
/// written in `ciphertext` are the tag guaranteeing integrity. `ciphertext` MUST have a capacity of
/// `plaintext.len()` + [TAG_LEN].
///
/// # Examples
///```rust
/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf_libcrux::{encrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
///
/// const PLAINTEXT_LEN: usize = 43;
/// let plaintext = "post-quantum cryptography is very important".as_bytes();
/// assert_eq!(PLAINTEXT_LEN, plaintext.len());
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
/// let mut ciphertext_buffer = [0u8; PLAINTEXT_LEN + TAG_LEN];
///
/// let res: anyhow::Result<()> = encrypt(&mut ciphertext_buffer, key, nonce, additional_data, plaintext);
/// assert!(res.is_ok());
/// # let expected_ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17,
/// # 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80,
/// # 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191,
/// # 8, 114, 85, 4, 25];
/// # assert_eq!(expected_ciphertext, &ciphertext_buffer);
///```
///
#[inline]
pub fn encrypt(
ciphertext: &mut [u8],
@@ -33,6 +63,33 @@ pub fn encrypt(
Ok(())
}
/// Decrypts a `ciphertext` and verifies the integrity of the `ciphertext` and the additional data
/// `ad`. using ChaCha20Poly1305 as implemented in [libcrux](https://github.com/cryspen/libcrux).
///
/// The `key` slice MUST have a length of [KEY_LEN]. The `nonce` slice MUST have a length of
/// [NONCE_LEN]. The plaintext buffer must have a capacity of `ciphertext.len()` - [TAG_LEN].
///
/// # Examples
///```rust
/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf_libcrux::{decrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
/// let ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17,
/// 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80,
/// 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191,
/// 8, 114, 85, 4, 25]; // this is the ciphertext generated by the example for the encryption
/// const PLAINTEXT_LEN: usize = 43;
/// assert_eq!(PLAINTEXT_LEN + TAG_LEN, ciphertext.len());
///
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
/// let mut plaintext_buffer = [0u8; PLAINTEXT_LEN];
///
/// let res: anyhow::Result<()> = decrypt(&mut plaintext_buffer, key, nonce, additional_data, ciphertext);
/// assert!(res.is_ok());
/// let expected_plaintext = "post-quantum cryptography is very important".as_bytes();
/// assert_eq!(expected_plaintext, plaintext_buffer);
///
///```
#[inline]
pub fn decrypt(
plaintext: &mut [u8],

View File

@@ -6,10 +6,15 @@ use rosenpass_to::{ops::copy_slice, with_destination, To};
use crate::subtle::blake2b;
/// The key length, 32 bytes or 256 bits.
pub const KEY_LEN: usize = 32;
/// The minimal key length, identical to [KEY_LEN]
pub const KEY_MIN: usize = KEY_LEN;
/// The maximal key length, identical to [KEY_LEN]
pub const KEY_MAX: usize = KEY_LEN;
/// The minimal output length, see [blake2b::OUT_MIN]
pub const OUT_MIN: usize = blake2b::OUT_MIN;
/// The maximal output length, see [blake2b::OUT_MAX]
pub const OUT_MAX: usize = blake2b::OUT_MAX;
/// This is a woefully incorrect implementation of hmac_blake2b.
@@ -19,6 +24,22 @@ pub const OUT_MAX: usize = blake2b::OUT_MAX;
///
/// This will be replaced, likely by Kekkac at some point soon.
/// <https://github.com/rosenpass/rosenpass/pull/145>
///
/// # Examples
///```rust
/// # use rosenpass_ciphers::subtle::incorrect_hmac_blake2b::hash;
/// use rosenpass_to::To;
/// let key: [u8; 32] = [0; 32];
/// let data: [u8; 32] = [255; 32];
/// // buffer for the hash output
/// let mut hash_data: [u8; 32] = [0u8; 32];
///
/// assert!(hash(&key, &data).to(&mut hash_data).is_ok(), "Hashing has to return OK result");
/// # let expected_hash: &[u8] = &[5, 152, 135, 141, 151, 106, 147, 8, 220, 95, 38, 66, 29, 33, 3,
/// 104, 250, 114, 131, 119, 27, 56, 59, 44, 11, 67, 230, 113, 112, 20, 80, 103];
/// # assert_eq!(hash_data, expected_hash);
///```
///
#[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];

View File

@@ -1,3 +1,9 @@
/// This module provides the following cryptographic schemes:
/// - [blake2b]: The blake2b hash function
/// - [chacha20poly1305_ietf]: The Chacha20Poly1305 AEAD as implemented in [RustCrypto](https://crates.io/crates/chacha20poly1305) (only used when the feature `experiment_libcrux` is disabled.
/// - [chacha20poly1305_ietf_libcrux]: The Chacha20Poly1305 AEAD as implemented in [libcrux](https://github.com/cryspen/libcrux) (only used when the feature `experiment_libcrux` is enabled.
/// - [incorrect_hmac_blake2b]: An (incorrect) hmac based on [blake2b].
/// - [xchacha20poly1305_ietf] The Chacha20Poly1305 AEAD as implemented in [RustCrypto](https://crates.io/crates/chacha20poly1305)
pub mod blake2b;
#[cfg(not(feature = "experiment_libcrux"))]
pub mod chacha20poly1305_ietf;

View File

@@ -6,10 +6,41 @@ use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::XChaCha20Poly1305 as AeadImpl;
use chacha20poly1305::{AeadCore, AeadInPlace, KeyInit, KeySizeUser};
/// The key length is 32 bytes or 256 bits.
pub const KEY_LEN: usize = typenum2const! { <AeadImpl as KeySizeUser>::KeySize };
/// The MAC tag length is 16 bytes or 128 bits.
pub const TAG_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::TagSize };
/// The nonce length is 24 bytes or 192 bits.
pub const NONCE_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::NonceSize };
/// Encrypts using XChaCha20Poly1305 as implemented in [RustCrypto](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305).
/// `key` and `nonce` MUST be chosen (pseudo-)randomly. The `key` slice MUST have a length of
/// [KEY_LEN]. The `nonce` slice MUST have a length of [NONCE_LEN].
/// In contrast to [chacha20poly1305_ietf::encrypt](crate::subtle::chacha20poly1305_ietf::encrypt) and
/// [chacha20poly1305_ietf_libcrux::encrypt](crate::subtle::chacha20poly1305_ietf_libcrux::encrypt),
/// `nonce` is also written into `ciphertext` and therefore ciphertext MUST have a length
/// of at least [NONCE_LEN] + `plaintext.len()` + [TAG_LEN].
///
/// # Examples
///```rust
/// # use rosenpass_ciphers::subtle::xchacha20poly1305_ietf::{encrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
/// const PLAINTEXT_LEN: usize = 43;
/// let plaintext = "post-quantum cryptography is very important".as_bytes();
/// assert_eq!(PLAINTEXT_LEN, plaintext.len());
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
/// let mut ciphertext_buffer = [0u8; NONCE_LEN + PLAINTEXT_LEN + TAG_LEN];
///
///
/// let res: anyhow::Result<()> = encrypt(&mut ciphertext_buffer, key, nonce, additional_data, plaintext);
/// # assert!(res.is_ok());
/// # let expected_ciphertext: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/// # 0, 0, 0, 0, 8, 241, 229, 253, 200, 81, 248, 30, 183, 149, 134, 168, 149, 87, 109, 49, 159, 108,
/// # 206, 89, 51, 232, 232, 197, 163, 253, 254, 208, 73, 76, 253, 13, 247, 162, 133, 184, 177, 44,
/// # 73, 138, 176, 193, 61, 248, 61, 183, 164, 192, 214, 168, 4, 1, 62, 243, 36, 48, 149, 164, 6];
/// # assert_eq!(expected_ciphertext, &ciphertext_buffer);
///```
#[inline]
pub fn encrypt(
ciphertext: &mut [u8],
@@ -28,6 +59,38 @@ pub fn encrypt(
Ok(())
}
/// Decrypts a `ciphertext` and verifies the integrity of the `ciphertext` and the additional data
/// `ad`. using XChaCha20Poly1305 as implemented in [RustCrypto](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305).
///
/// The `key` slice MUST have a length of [KEY_LEN]. The `nonce` slice MUST have a length of
/// [NONCE_LEN]. The plaintext buffer must have a capacity of `ciphertext.len()` - [TAG_LEN] - [NONCE_LEN].
///
/// In contrast to [chacha20poly1305_ietf::decrypt](crate::subtle::chacha20poly1305_ietf::decrypt) and
/// [chacha20poly1305_ietf_libcrux::decrypt](crate::subtle::chacha20poly1305_ietf_libcrux::decrypt),
/// `ciperhtext` MUST include the as it is not given otherwise.
///
/// # Examples
///```rust
/// # use rosenpass_ciphers::subtle::xchacha20poly1305_ietf::{decrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
/// let ciphertext: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/// # 0, 0, 0, 0, 8, 241, 229, 253, 200, 81, 248, 30, 183, 149, 134, 168, 149, 87, 109, 49, 159, 108,
/// # 206, 89, 51, 232, 232, 197, 163, 253, 254, 208, 73, 76, 253, 13, 247, 162, 133, 184, 177, 44,
/// # 73, 138, 176, 193, 61, 248, 61, 183, 164, 192, 214, 168, 4, 1, 62, 243, 36, 48, 149, 164, 6];
/// // this is the ciphertext generated by the example for the encryption
/// const PLAINTEXT_LEN: usize = 43;
/// assert_eq!(PLAINTEXT_LEN + TAG_LEN + NONCE_LEN, ciphertext.len());
///
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
/// let mut plaintext_buffer = [0u8; PLAINTEXT_LEN];
///
/// let res: anyhow::Result<()> = decrypt(&mut plaintext_buffer, key, additional_data, ciphertext);
/// assert!(res.is_ok());
/// let expected_plaintext = "post-quantum cryptography is very important".as_bytes();
/// assert_eq!(expected_plaintext, plaintext_buffer);
///
///```
#[inline]
pub fn decrypt(
plaintext: &mut [u8],

44
coverage_report.sh Executable file
View File

@@ -0,0 +1,44 @@
#! /usr/bin/env bash
set -e -o pipefail
OUTPUT_DIR="target/grcov"
log() {
echo >&2 "$@"
}
exc() {
echo '$' "$@"
"$@"
}
main() {
exc cd "$(dirname "$0")"
local open="0"
if [[ "$1" == "--open" ]]; then
open="1"
fi
exc cargo llvm-cov --all-features --workspace --doctests
exc rm -rf "${OUTPUT_DIR}"
exc mkdir -p "${OUTPUT_DIR}"
exc grcov target/llvm-cov-target/ --llvm -s . --branch \
--binary-path ./target/llvm-cov-target/debug/deps \
--ignore-not-existing --ignore '../*' --ignore "/*" \
--excl-line '^\s*#\[(derive|repr)\(' \
-t lcov,html,markdown -o "${OUTPUT_DIR}"
if (( "${open}" == 1 )); then
xdg-open "${PWD}/${OUTPUT_DIR}/html/index.html"
fi
log ""
log "Generated reports in \"${PWD}/${OUTPUT_DIR}\"."
log "Open \"${PWD}/${OUTPUT_DIR}/html/index.html\" to view HTML report."
log ""
}
main "$@"

View File

@@ -109,11 +109,27 @@
proverif-patched
];
};
# TODO: Write this as a patched version of the default environment
devShells.fullEnv = pkgs.mkShell {
inherit (pkgs.proof-proverif) CRYPTOVERIF_LIB;
inputsFrom = [ pkgs.rosenpass ];
nativeBuildInputs = with pkgs; [
cargo-release
rustfmt
nodePackages.prettier
nushell # for the .ci/gen-workflow-files.nu script
proverif-patched
inputs.fenix.packages.${system}.complete.toolchain
pkgs.cargo-llvm-cov
pkgs.grcov
];
};
devShells.coverage = pkgs.mkShell {
inputsFrom = [ pkgs.rosenpass ];
nativeBuildInputs = [
inputs.fenix.packages.${system}.complete.toolchain
pkgs.cargo-llvm-cov
pkgs.grcov
];
};

View File

@@ -2,8 +2,8 @@
template: rosenpass
title: Rosenpass
author:
- Karolin Varner = Independent Researcher
- Benjamin Lipp = Max Planck Institute for Security and Privacy (MPI-SP)
- Karolin Varner = Rosenpass e.V., Max Planck Institute for Security and Privacy (MPI-SP)
- Benjamin Lipp = Rosenpass e.V., Max Planck Institute for Security and Privacy (MPI-SP)
- Wanja Zaeske
- Lisa Schmidt = {Scientific Illustrator \\url{mullana.de}}
- Prabhpreet Dua
@@ -383,9 +383,18 @@ fn load_biscuit(nct) {
"biscuit additional data",
spkr, sidi, sidr);
let pt : Biscuit = XAEAD::dec(k, n, ct, ad);
// Find the peer and apply retransmission protection
lookup_peer(pt.peerid);
assert(pt.biscuit_no <= peer.biscuit_used);
// In December 2024, the InitConf retransmission mechanisim was redesigned
// in a backwards-compatible way. See the changelog.
//
// -- 2024-11-30, Karolin Varner
if (protocol_version!(< "0.3.0")) {
// Ensure that the biscuit is used only once
assert(pt.biscuit_no <= peer.biscuit_used);
}
// Restore the chaining key
ck ← pt.ck;
@@ -501,7 +510,7 @@ LAST_UNDER_LOAD_WINDOW = 1 //seconds
The initiator deals with packet loss by storing the messages it sends to the responder and retransmitting them in randomized, exponentially increasing intervals until they get a response. Receiving RespHello terminates retransmission of InitHello. A Data or EmptyData message serves as acknowledgement of receiving InitConf and terminates its retransmission.
The responder does not need to do anything special to handle RespHello retransmission if the RespHello package is lost, the initiator retransmits InitHello and the responder can generate another RespHello package from that. InitConf retransmission needs to be handled specifically in the responder code because accepting an InitConf retransmission would reset the live session including the nonce counter, which would cause nonce reuse. Implementations must detect the case that `biscuit_no = biscuit_used` in ICR5, skip execution of ICR6 and ICR7, and just transmit another EmptyData package to confirm that the initiator can stop transmitting InitConf.
The responder uses less complex form of the same mechanism: The responder never retransmits RespHello, instead the responder generates a new RespHello message if InitHello is retransmitted. Responder confirmation messages of completed handshake (EmptyData) messages are retransmitted by storing the most recent InitConf messages (or their hashes) and caching the associated EmptyData messages. Through this cache, InitConf retransmission is detected and the associated EmptyData message is retransmitted.
### Interaction with cookie reply system
@@ -515,6 +524,76 @@ When the responder is under load and it recieves an InitConf message, the messag
# Changelog
### 0.3.x
#### 2024-10-30 InitConf retransmission updates
\vspace{0.5em}
Author: Karolin Varner
Issue: [#331](https://github.com/rosenpass/rosenpass/issues/331)
PR: [#513](https://github.com/rosenpass/rosenpass/pull/513)
\vspace{0.5em}
We redesign the InitConf retransmission mechanism to use a hash table. This avoids the need for the InitConf handling code to account for InitConf retransmission specifically and moves the retransmission logic into less-sensitive code.
Previously, we would specifically account for InitConf retransmission in the InitConf handling code by checking the biscuit number: If the biscuit number was higher than any previously seen biscuit number, then this must be a new key-exchange being completed; if the biscuit number was exactly the highest seen biscuit number, then the InitConf message is interpreted as an InitConf retransmission; in this case, an entirely new EmptyData (responder confirmation) message was generated as confirmation that InitConf has been received and that the initiator can now cease opportunistic retransmission of InitConf.
This mechanism was a bit brittle; even leading to a very minor but still relevant security issue, necessitating the release of Rosenpass maintenance version 0.2.2 with a [fix for the problem](https://github.com/rosenpass/rosenpass/pull/329). We had processed the InitConf message, correctly identifying that InitConf was a retransmission, but we failed to pass this information on to the rest of the code base, leading to double emission of the same "hey, we have a new cryptographic session key" even if the `outfile` option was used to integrate Rosenpass into some external application. If this event was used anywhere to reset a nonce, then this could have led to a nonce-misuse, although for the use with WireGuard this is not an issue.
By removing all retransmission handling code from the cryptographic protocol, we are taking structural measures to exclude the possibilities of similar issues.
- In section "Dealing With Package Loss" we replace
\begin{quote}
The responder does not need to do anything special to handle RespHello retransmission if the RespHello package is lost, the initiator retransmits InitHello and the responder can generate another RespHello package from that. InitConf retransmission needs to be handled specifically in the responder code because accepting an InitConf retransmission would reset the live session including the nonce counter, which would cause nonce reuse. Implementations must detect the case that `biscuit_no = biscuit_used` in ICR5, skip execution of ICR6 and ICR7, and just transmit another EmptyData package to confirm that the initiator can stop transmitting InitConf.
\end{quote}
by
\begin{quote}
The responder uses less complex form of the same mechanism: The responder never retransmits RespHello, instead the responder generates a new RespHello message if InitHello is retransmitted. Responder confirmation messages of completed handshake (EmptyData) messages are retransmitted by storing the most recent InitConf messages (or their hashes) and caching the associated EmptyData messages. Through this cache, InitConf retransmission is detected and the associated EmptyData message is retransmitted.
\end{quote}
- In function `load_biscuit` we replace
``` {=tex}
\begin{quote}
\begin{minted}{pseudorust}
assert(pt.biscuit_no <= peer.biscuit_used);
\end{minted}
\end{quote}
```
by
``` {=tex}
\begin{quote}
\begin{minted}{pseudorust}
// In December 2024, the InitConf retransmission mechanisim was redesigned
// in a backwards-compatible way. See the changelog.
//
// -- 2024-11-30, Karolin Varner
if (protocol_version!(< "0.3.0")) {
// Ensure that the biscuit is used only once
assert(pt.biscuit_no <= peer.biscuit_used);
}
\end{minted}
\end{quote}
```
#### 2024-04-16 Denial of Service Mitigation
\vspace{0.5em}
Author: Prabhpreet Dua
Issue: [#137](https://github.com/rosenpass/rosenpass/issues/137)
PR: [#142](https://github.com/rosenpass/rosenpass/pull/142)
\vspace{0.5em}
- Added denial of service mitigation using the WireGuard cookie mechanism
- Added section "Denial of Service Mitigation and Cookies", and modify "Dealing with Packet Loss" for DoS cookie mechanism
\printbibliography

View File

@@ -23,6 +23,12 @@ rosenpass help
Follow [quick start instructions](https://rosenpass.eu/#start) to get a VPN up and running.
## Contributing
Contributions are generally welcome. Join our [Matrix Chat](https://matrix.to/#/#rosenpass:matrix.org) if you are looking for guidance on how to contribute or for people to collaborate with.
We also have a as of now, very minimal [contributors guide](CONTRIBUTING.md).
## Software architecture
The [rosenpass tool](./src/) is written in Rust and uses liboqs[^liboqs]. The tool establishes a symmetric key and provides it to WireGuard. Since it supplies WireGuard with key through the PSK feature using Rosenpass+WireGuard is cryptographically no less secure than using WireGuard on its own ("hybrid security"). Rosenpass refreshes the symmetric key every two minutes.

View File

@@ -62,6 +62,7 @@ heck = { workspace = true, optional = true }
command-fds = { workspace = true, optional = true }
rustix = { workspace = true, optional = true }
uds = { workspace = true, optional = true, features = ["mio_1xx"] }
signal-hook = { workspace = true, optional = true }
[build-dependencies]
anyhow = { workspace = true }
@@ -87,5 +88,6 @@ experiment_api = [
"rosenpass-util/experiment_file_descriptor_passing",
"rosenpass-wireguard-broker/experiment_api",
]
internal_signal_handling_for_coverage_reports = ["signal-hook"]
internal_testing = []
internal_bin_gen_ipc_msg_types = ["hex", "heck"]

View File

@@ -1,3 +1,5 @@
//! The bulk code relating to the Rosenpass unix socket API
mod api_handler;
mod boilerplate;

View File

@@ -3,6 +3,7 @@ use heck::ToShoutySnakeCase;
use rosenpass_ciphers::{hash_domain::HashDomain, KEY_LEN};
/// Recursively calculate a concrete hash value for an API message type
fn calculate_hash_value(hd: HashDomain, values: &[&str]) -> Result<[u8; KEY_LEN]> {
match values.split_first() {
Some((head, tail)) => calculate_hash_value(hd.mix(head.as_bytes())?, tail),
@@ -10,6 +11,7 @@ fn calculate_hash_value(hd: HashDomain, values: &[&str]) -> Result<[u8; KEY_LEN]
}
}
/// Print a hash literal for pasting into the Rosenpass source code
fn print_literal(path: &[&str]) -> Result<()> {
let val = calculate_hash_value(HashDomain::zero(), path)?;
let (last, prefix) = path.split_last().context("developer error!")?;
@@ -33,6 +35,8 @@ fn print_literal(path: &[&str]) -> Result<()> {
Ok(())
}
/// Tree of domain separators where each leaf represents
/// an API message ID
#[derive(Debug, Clone)]
enum Tree {
Branch(String, Vec<Tree>),
@@ -68,6 +72,7 @@ impl Tree {
}
}
/// Helper for generating hash-based message IDs for the IPC API
fn main() -> Result<()> {
let tree = Tree::Branch(
"Rosenpass IPC API".to_owned(),

View File

@@ -1,13 +1,68 @@
//! Pseudo Random Functions (PRFs) with a tree-like label scheme which
//! ensures their uniqueness
//! ensures their uniqueness.
//!
//! This ensures [domain separation](https://en.wikipedia.org/wiki/Domain_separation) is used
//! across the Rosenpass protocol.
//!
//! There is a chart containing all hash domains used in Rosenpass in the
//! [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository).
//!
//! # Tutorial
//!
//! ```
//! use rosenpass::{hash_domain, hash_domain_ns};
//! use rosenpass::hash_domains::protocol;
//!
//! // Declaring a custom hash domain
//! hash_domain_ns!(protocol, custom_domain, "my custom hash domain label");
//!
//! // Declaring a custom hashers
//! hash_domain_ns!(custom_domain, hashers, "hashers");
//! hash_domain_ns!(hashers, hasher1, "1");
//! hash_domain_ns!(hashers, hasher2, "2");
//!
//! // Declaring specific domain separators
//! hash_domain_ns!(custom_domain, domain_separators, "domain separators");
//! hash_domain!(domain_separators, sep1, "1");
//! hash_domain!(domain_separators, sep2, "2");
//!
//! // Generating values under hasher1 with both domain separators
//! let h1 = hasher1()?.mix(b"some data")?.dup();
//! let h1v1 = h1.mix(&sep1()?)?.mix(b"More data")?.into_value();
//! let h1v2 = h1.mix(&sep2()?)?.mix(b"More data")?.into_value();
//!
//! // Generating values under hasher2 with both domain separators
//! let h2 = hasher2()?.mix(b"some data")?.dup();
//! let h2v1 = h2.mix(&sep1()?)?.mix(b"More data")?.into_value();
//! let h2v2 = h2.mix(&sep2()?)?.mix(b"More data")?.into_value();
//!
//! // All of the domain separators are now different, random strings
//! let values = [h1v1, h1v2, h2v1, h2v2];
//! for i in 0..values.len() {
//! for j in (i+1)..values.len() {
//! assert_ne!(values[i], values[j]);
//! }
//! }
//!
//! Ok::<(), anyhow::Error>(())
//! ```
use anyhow::Result;
use rosenpass_ciphers::{hash_domain::HashDomain, KEY_LEN};
use rosenpass_ciphers::hash_domain::HashDomain;
/// Declare a hash function
///
/// # Examples
///
/// See the source file for details about how this is used concretely.
///
/// See the [module](self) documentation on how to use the hash domains in general
// TODO Use labels that can serve as identifiers
#[macro_export]
macro_rules! hash_domain_ns {
($base:ident, $name:ident, $($lbl:expr),* ) => {
pub fn $name() -> Result<HashDomain> {
($(#[$($attrss:tt)*])* $base:ident, $name:ident, $($lbl:expr),+ ) => {
$(#[$($attrss)*])*
pub fn $name() -> ::anyhow::Result<::rosenpass_ciphers::hash_domain::HashDomain> {
let t = $base()?;
$( let t = t.mix($lbl.as_bytes())?; )*
Ok(t)
@@ -15,9 +70,18 @@ macro_rules! hash_domain_ns {
}
}
/// Declare a concrete hash value
///
/// # Examples
///
/// See the source file for details about how this is used concretely.
///
/// See the [module](self) documentation on how to use the hash domains in general
#[macro_export]
macro_rules! hash_domain {
($base:ident, $name:ident, $($lbl:expr),* ) => {
pub fn $name() -> Result<[u8; KEY_LEN]> {
($(#[$($attrss:tt)*])* $base:ident, $name:ident, $($lbl:expr),+ ) => {
$(#[$($attrss)*])*
pub fn $name() -> ::anyhow::Result<[u8; ::rosenpass_ciphers::KEY_LEN]> {
let t = $base()?;
$( let t = t.mix($lbl.as_bytes())?; )*
Ok(t.into_value())
@@ -25,24 +89,227 @@ macro_rules! hash_domain {
}
}
/// The hash domain containing the protocol string.
///
/// This serves as a global [domain separator](https://en.wikipedia.org/wiki/Domain_separation)
/// used in various places in the rosenpass protocol.
///
/// This is generally used to create further hash-domains for specific purposes. See
///
/// # Examples
///
/// See the source file for details about how this is used concretely.
///
/// See the [module](self) documentation on how to use the hash domains in general
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, cookie_value, "cookie-value");
hash_domain_ns!(protocol, cookie_key, "cookie-key");
hash_domain_ns!(protocol, peerid, "peer id");
hash_domain_ns!(protocol, biscuit_ad, "biscuit additional data");
hash_domain_ns!(protocol, ckinit, "chaining key init");
hash_domain_ns!(protocol, _ckextract, "chaining key extract");
hash_domain_ns!(
/// Hash domain based on [protocol] for calculating [crate::msgs::Envelope::mac].
///
/// # Examples
///
/// See the source of [crate::msgs::Envelope::seal] and [crate::msgs::Envelope::check_seal]
/// to figure out how this is concretely used.
///
/// See the [module](self) documentation on how to use the hash domains in general.
protocol, mac, "mac");
hash_domain_ns!(
/// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie].
///
/// # Examples
///
/// See the source of [crate::msgs::Envelope::seal_cookie],
/// [crate::protocol::CryptoServer::handle_msg_under_load], and
/// [crate::protocol::CryptoServer::handle_cookie_reply]
/// to figure out how this is concretely used.
///
/// See the [module](self) documentation on how to use the hash domains in general.
protocol, cookie, "cookie");
hash_domain_ns!(
/// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie].
///
/// # Examples
///
/// See the source of [crate::msgs::Envelope::seal_cookie],
/// [crate::protocol::CryptoServer::handle_msg_under_load], and
/// [crate::protocol::CryptoServer::handle_cookie_reply]
/// to figure out how this is concretely used.
///
/// See the [module](self) documentation on how to use the hash domains in general.
protocol, cookie_value, "cookie-value");
hash_domain_ns!(
/// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie].
///
/// # Examples
///
/// See the source of [crate::msgs::Envelope::seal_cookie],
/// [crate::protocol::CryptoServer::handle_msg_under_load], and
/// [crate::protocol::CryptoServer::handle_cookie_reply]
/// to figure out how this is concretely used.
///
/// See the [module](self) documentation on how to use the hash domains in general.
protocol, cookie_key, "cookie-key");
hash_domain_ns!(
/// Hash domain based on [protocol] for calculating the peer id as transmitted (encrypted)
/// in [crate::msgs::InitHello::pidic].
///
/// # Examples
///
/// See the source of [crate::protocol::CryptoServer::pidm] and
/// [crate::protocol::Peer::pidt]
/// to figure out how this is concretely used.
///
/// See the [module](self) documentation on how to use the hash domains in general.
protocol, peerid, "peer id");
hash_domain_ns!(
/// Hash domain based on [protocol] for calculating the additional data
/// during [crate::msgs::Biscuit] encryption, storing the biscuit into
/// [crate::msgs::RespHello::biscuit].
///
/// # Examples
///
/// To understand how the biscuit is used, it is best to read
/// the code of [crate::protocol::HandshakeState::store_biscuit] and
/// [crate::protocol::HandshakeState::load_biscuit]
///
/// See the [module](self) documentation on how to use the hash domains in general.
protocol, biscuit_ad, "biscuit additional data");
hash_domain_ns!(
/// This hash domain begins our actual handshake procedure, initializing the
/// chaining key [crate::protocol::HandshakeState::ck].
///
/// # Examples
///
/// To understand how the chaining key is used, study
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
/// and [crate::protocol::HandshakeState::mix].
///
/// See the [module](self) documentation on how to use the hash domains in general.
protocol, ckinit, "chaining key init");
hash_domain_ns!(
/// Namespace for chaining key usage domain separators.
///
/// During the execution of the Rosenpass protocol, we use the chaining key for multiple
/// purposes, so to make sure that we have unique value domains, we mix a domain separator
/// into the chaining key before using it for any particular purpose.
///
/// We could use the full domain separation strings, but using a hash value here is nice
/// because it does not lead to any constraints about domain separator format and we can
/// even allow third parties to define their own separators by claiming a namespace.
///
/// # Examples
///
/// To understand how the chaining key is used, study
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
/// and [crate::protocol::HandshakeState::mix].
///
/// See the [module](self) documentation on how to use the hash domains in general.
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!(
/// Used to mix in further values into the chaining key during the handshake.
///
/// See [_ckextract].
///
/// # Examples
///
/// To understand how the chaining key is used, study
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
/// and [crate::protocol::HandshakeState::mix].
///
/// See the [module](self) documentation on how to use the hash domains in general.
_ckextract, mix, "mix");
hash_domain!(
/// Chaining key domain separator for generating encryption keys that can
/// encrypt parts of the handshake.
///
/// See [_ckextract].
///
/// # Examples
///
/// Encryption of data during the handshake happens in
/// [crate::protocol::HandshakeState::encrypt_and_mix] and decryption happens in
/// [crate::protocol::HandshakeState::decrypt_and_mix]. See their source code
/// for details.
///
/// To understand how the chaining key is used, study
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
/// and [crate::protocol::HandshakeState::mix].
///
/// See the [module](self) documentation on how to use the hash domains in general.
_ckextract, hs_enc, "handshake encryption");
hash_domain!(
/// Chaining key domain separator for live data encryption.
/// Live data encryption is only used to send confirmation of handshake
/// done in [crate::msgs::EmptyData].
///
/// See [_ckextract].
///
/// # Examples
///
/// This domain separator finds use in [crate::protocol::HandshakeState::enter_live].
///
/// To understand how the chaining key is used, study
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
/// and [crate::protocol::HandshakeState::mix].
///
/// See the [module](self) documentation on how to use the hash domains in general.
_ckextract, ini_enc, "initiator handshake encryption");
hash_domain!(
/// Chaining key domain separator for live data encryption.
/// Live data encryption is only used to send confirmation of handshake
/// done in [crate::msgs::EmptyData].
///
/// See [_ckextract].
///
/// # Examples
///
/// This domain separator finds use in [crate::protocol::HandshakeState::enter_live].
/// Check out its source code!
///
/// To understand how the chaining key is used, study
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
/// and [crate::protocol::HandshakeState::mix].
///
/// See the [module](self) documentation on how to use the hash domains in general.
_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");
hash_domain_ns!(
/// Chaining key domain separator for any usage specific purposes.
///
/// We do recommend that third parties base their specific domain separators
/// on a internet domain and/or mix in much more specific information.
///
/// We only really use this to derive a output key for wireguard; see [osk].
///
/// See [_ckextract].
///
/// # Examples
///
/// See the [module](self) documentation on how to use the hash domains in general.
_ckextract, _user, "user");
hash_domain_ns!(
/// Chaining key domain separator for any rosenpass specific purposes.
///
/// We only really use this to derive a output key for wireguard; see [osk].
///
/// See [_ckextract].
///
/// # Examples
///
/// See the [module](self) documentation on how to use the hash domains in general.
_user, _rp, "rosenpass.eu");
hash_domain!(
/// Chaining key domain separator for deriving the key sent to WireGuard.
///
/// See [_ckextract].
///
/// # Examples
///
/// This domain separator finds use in [crate::protocol::CryptoServer::osk].
/// Check out its source code!
///
/// See the [module](self) documentation on how to use the hash domains in general.
_rp, osk, "wireguard psk");

View File

@@ -1,3 +1,18 @@
//! This is the central rosenpass crate implementing the rosenpass protocol.
//!
//! - [crate::app_server] contains the business logic of rosenpass, handling networking
//! - [crate::cli] contains the cli parsing logic and contains quite a bit of startup logic; the
//! main function quickly hands over to [crate::cli::CliArgs::run] which contains quite a bit
//! of our startup logic
//! - [crate::config] has the code to parse and generate configuration files
//! - [crate::hash_domains] lists the different hash function domains used in the Rosenpass
//! protocol
//! - [crate::msgs] provides declarations of the Rosenpass protocol network messages and facilities
//! to parse those messages through the [::zerocopy] crate
//! - [crate::protocol] this is where the bulk of our code lives; this module contains the actual
//! cryptographic protocol logic
//! - crate::api implements the Rosenpass unix socket API, if feature "experiment_api" is active
#[cfg(feature = "experiment_api")]
pub mod api;
pub mod app_server;
@@ -7,14 +22,25 @@ pub mod hash_domains;
pub mod msgs;
pub mod protocol;
/// Error types used in diverse places across Rosenpass
#[derive(thiserror::Error, Debug)]
pub enum RosenpassError {
/// Usually indicates that parsing a struct through the
/// [::zerocopy] crate failed
#[error("buffer size mismatch")]
BufferSizeMismatch,
/// Mostly raised by the `TryFrom<u8>` implementation for [crate::msgs::MsgType]
/// to indicate that a message type is not defined
#[error("invalid message type")]
InvalidMessageType(u8),
InvalidMessageType(
/// The message type that could not be parsed
u8,
),
/// Raised by the `TryFrom<RawMsgType>` (crate::api::RawMsgType) implementation for crate::api::RequestMsgType
/// and crate::api::RequestMsgType to indicate that a message type is not defined
#[error("invalid API message type")]
InvalidApiMessageType(u128),
#[error("could not parse API message")]
InvalidApiMessage,
InvalidApiMessageType(
/// The message type that could not be parsed
u128,
),
}

View File

@@ -1,10 +1,14 @@
//! For the main function
use clap::CommandFactory;
use clap::Parser;
use clap_mangen::roff::{roman, Roff};
use log::error;
use rosenpass::cli::CliArgs;
use rosenpass_util::functional::run;
use std::process::exit;
/// Printing custom man sections when generating the man page
fn print_custom_man_section(section: &str, text: &str, file: &mut std::fs::File) {
let mut roff = Roff::default();
roff.control("SH", [section]);
@@ -13,6 +17,8 @@ fn print_custom_man_section(section: &str, text: &str, file: &mut std::fs::File)
}
/// Catches errors, prints them through the logger, then exits
///
/// The bulk of the command line logic is handled inside [crate::cli::CliArgs::run].
pub fn main() {
// parse CLI arguments
let args = CliArgs::parse();
@@ -72,30 +78,107 @@ pub fn main() {
// error!("error dummy");
}
let broker_interface = args.get_broker_interface();
match args.run(broker_interface, None) {
Ok(_) => {}
Err(e) => {
error!("{e:?}");
exit(1);
let res = run(|| {
#[cfg(feature = "internal_signal_handling_for_coverage_reports")]
let term_signal = terminate::TerminateRequested::new()?;
let broker_interface = args.get_broker_interface();
let err = match args.run(broker_interface, None) {
Ok(()) => return Ok(()),
Err(err) => err,
};
// This is very very hacky and just used for coverage measurement
#[cfg(feature = "internal_signal_handling_for_coverage_reports")]
{
let terminated_by_signal = err
.downcast_ref::<std::io::Error>()
.filter(|e| e.kind() == std::io::ErrorKind::Interrupted)
.filter(|_| term_signal.value())
.is_some();
if terminated_by_signal {
log::warn!(
"\
Terminated by signal; this signal handler is correct during coverage testing \
but should be otherwise disabled"
);
return Ok(());
}
}
Err(err)
});
if let Err(e) = res {
error!("{e:?}");
exit(1);
}
}
/// Custom main page section: Exit Status
static EXIT_STATUS_MAN: &str = r"
The rosenpass utility exits 0 on success, and >0 if an error occurs.";
/// Custom main page section: See also.
static SEE_ALSO_MAN: &str = r"
rp(1), wg(1)
Karolin Varner, Benjamin Lipp, Wanja Zaeske, and Lisa Schmidt, Rosenpass, https://rosenpass.eu/whitepaper.pdf, 2023.";
/// Custom main page section: Standards.
static STANDARDS_MAN: &str = r"
This tool is the reference implementation of the Rosenpass protocol, as
specified within the whitepaper referenced above.";
/// Custom main page section: Authors.
static AUTHORS_MAN: &str = r"
Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske, Marei
Peischl, Stephan Ajuvo, and Lisa Schmidt.";
/// Custom main page section: Bugs.
static BUGS_MAN: &str = r"
The bugs are tracked at https://github.com/rosenpass/rosenpass/issues.";
/// These signal handlers are used exclusively used during coverage testing
/// to ensure that the llvm-cov can produce reports during integration tests
/// with multiple processes where subprocesses are terminated via kill(2).
///
/// llvm-cov does not support producing coverage reports when the process exits
/// through a signal, so this is necessary.
///
/// The functionality of exiting gracefully upon reception of a terminating signal
/// is desired for the production variant of Rosenpass, but we should make sure
/// to use a higher quality implementation; in particular, we should use signalfd(2).
///
#[cfg(feature = "internal_signal_handling_for_coverage_reports")]
mod terminate {
use signal_hook::flag::register as sig_register;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
/// Automatically register a signal handler for common termination signals;
/// whether one of these signals was issued can be polled using [Self::value].
///
/// The signal handler is not removed when this struct goes out of scope.
pub struct TerminateRequested {
value: Arc<AtomicBool>,
}
impl TerminateRequested {
/// Register signal handlers watching for common termination signals
pub fn new() -> anyhow::Result<Self> {
let value = Arc::new(AtomicBool::new(false));
for sig in signal_hook::consts::TERM_SIGNALS.iter().copied() {
sig_register(sig, Arc::clone(&value))?;
}
Ok(Self { value })
}
/// Check whether a termination signal has been set
pub fn value(&self) -> bool {
self.value.load(Ordering::Relaxed)
}
}
}

View File

@@ -9,20 +9,75 @@
//! To achieve this we utilize the zerocopy library.
//!
use std::mem::size_of;
use std::u8;
use zerocopy::{AsBytes, FromBytes, FromZeroes};
use super::RosenpassError;
use rosenpass_cipher_traits::Kem;
use rosenpass_ciphers::kem::{EphemeralKem, StaticKem};
use rosenpass_ciphers::{aead, xaead, KEY_LEN};
pub const MSG_SIZE_LEN: usize = 1;
pub const RESERVED_LEN: usize = 3;
pub const MAC_SIZE: usize = 16;
pub const COOKIE_SIZE: usize = 16;
pub const SID_LEN: usize = 4;
/// Length of a session ID such as [InitHello::sidi]
pub const SESSION_ID_LEN: usize = 4;
/// Length of a biscuit ID; i.e. size of the value in [Biscuit::biscuit_no]
pub const BISCUIT_ID_LEN: usize = 12;
/// TODO: Unused, remove!
pub const WIRE_ENVELOPE_LEN: usize = 1 + 3 + 16 + 16; // TODO verify this
/// Size required to fit any message in binary form
pub const MAX_MESSAGE_LEN: usize = 2500; // TODO fix this
/// length in bytes of an unencrypted Biscuit (plain text)
pub const BISCUIT_PT_LEN: usize = size_of::<Biscuit>();
/// Length in bytes of an encrypted Biscuit (cipher text)
pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + xaead::NONCE_LEN + xaead::TAG_LEN;
/// Size of the field [Envelope::mac]
pub const MAC_SIZE: usize = 16;
/// Size of the field [Envelope::cookie]
pub const COOKIE_SIZE: usize = MAC_SIZE;
/// Type of the mac field in [Envelope]
pub type MsgEnvelopeMac = [u8; MAC_SIZE];
/// Type of the cookie field in [Envelope]
pub type MsgEnvelopeCookie = [u8; COOKIE_SIZE];
/// Header and footer included in all our packages,
/// including a type field.
///
/// # Examples
///
/// ```
/// use rosenpass::msgs::{Envelope, InitHello};
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
/// use memoffset::offset_of;
///
/// // Zero-initialization
/// let mut ih = Envelope::<InitHello>::new_zeroed();
///
/// // Edit fields normally
/// ih.mac[0] = 1;
///
/// // Edit as binary
/// ih.as_bytes_mut()[offset_of!(Envelope<InitHello>, msg_type)] = 23;
/// assert_eq!(ih.msg_type, 23);;
///
/// // Conversion to bytes
/// let mut ih2 = ih.as_bytes().to_owned();
///
/// // Setting msg_type field, again
/// ih2[0] = 42;
///
/// // Zerocopy parsing
/// let ih3 = Ref::<&mut [u8], Envelope<InitHello>>::new(&mut ih2).unwrap();
/// assert_ne!(ih.as_bytes(), ih3.as_bytes());
/// assert_eq!(ih3.msg_type, 42);
/// ```
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
#[derive(AsBytes, FromBytes, FromZeroes, Clone)]
pub struct Envelope<M: AsBytes + FromBytes> {
/// [MsgType] of this message
pub msg_type: u8,
@@ -32,11 +87,45 @@ pub struct Envelope<M: AsBytes + FromBytes> {
pub payload: M,
/// Message Authentication Code (mac) over all bytes until (exclusive)
/// `mac` itself
pub mac: [u8; 16],
pub mac: MsgEnvelopeMac,
/// Currently unused, TODO: do something with this
pub cookie: [u8; 16],
pub cookie: MsgEnvelopeCookie,
}
/// This is the first message sent by the initiator to the responder
/// during the execution of the Rosenpass protocol.
///
/// When transmitted on the wire, this type will generally be wrapped into [Envelope].
///
/// # Examples
///
/// Check out the code of [crate::protocol::CryptoServer::handle_initiation] (generation on
/// iniatiator side) and [crate::protocol::CryptoServer::handle_init_hello] (processing on
/// responder side) to understand how this is used.
///
/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate.
///
/// ```
/// use rosenpass::msgs::{Envelope, InitHello};
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
/// use memoffset::span_of;
///
/// // Zero initialization
/// let mut ih = Envelope::<InitHello>::new_zeroed();
///
/// // Conversion to byte representation
/// let ih = ih.as_bytes_mut();
///
/// // Set value on byte representation
/// ih[span_of!(Envelope<InitHello>, payload)][span_of!(InitHello, sidi)]
/// .copy_from_slice(&[1,2,3,4]);
///
/// // Conversion from bytes
/// let ih = Ref::<&mut [u8], Envelope<InitHello>>::new(ih).unwrap();
///
/// // Check that write above on byte representation was effective
/// assert_eq!(ih.payload.sidi, [1,2,3,4]);
/// ```
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct InitHello {
@@ -52,6 +141,40 @@ pub struct InitHello {
pub auth: [u8; aead::TAG_LEN],
}
/// This is the second message sent by the responder to the initiator
/// during the execution of the Rosenpass protocol in response to [InitHello].
///
/// When transmitted on the wire, this type will generally be wrapped into [Envelope].
///
/// # Examples
///
/// Check out the code of [crate::protocol::CryptoServer::handle_init_hello] (generation on
/// responder side) and [crate::protocol::CryptoServer::handle_resp_hello] (processing on
/// initiator side) to understand how this is used.
///
/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate.
///
/// ```
/// use rosenpass::msgs::{Envelope, RespHello};
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
/// use memoffset::span_of;
///
/// // Zero initialization
/// let mut ih = Envelope::<RespHello>::new_zeroed();
///
/// // Conversion to byte representation
/// let ih = ih.as_bytes_mut();
///
/// // Set value on byte representation
/// ih[span_of!(Envelope<RespHello>, payload)][span_of!(RespHello, sidi)]
/// .copy_from_slice(&[1,2,3,4]);
///
/// // Conversion from bytes
/// let ih = Ref::<&mut [u8], Envelope<RespHello>>::new(ih).unwrap();
///
/// // Check that write above on byte representation was effective
/// assert_eq!(ih.payload.sidi, [1,2,3,4]);
/// ```
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct RespHello {
@@ -69,8 +192,42 @@ pub struct RespHello {
pub biscuit: [u8; BISCUIT_CT_LEN],
}
/// This is the third message sent by the initiator to the responder
/// during the execution of the Rosenpass protocol in response to [RespHello].
///
/// When transmitted on the wire, this type will generally be wrapped into [Envelope].
///
/// # Examples
///
/// Check out the code of [crate::protocol::CryptoServer::handle_resp_hello] (generation on
/// initiator side) and [crate::protocol::CryptoServer::handle_init_conf] (processing on
/// responder side) to understand how this is used.
///
/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate.
///
/// ```
/// use rosenpass::msgs::{Envelope, InitConf};
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
/// use memoffset::span_of;
///
/// // Zero initialization
/// let mut ih = Envelope::<InitConf>::new_zeroed();
///
/// // Conversion to byte representation
/// let ih = ih.as_bytes_mut();
///
/// // Set value on byte representation
/// ih[span_of!(Envelope<InitConf>, payload)][span_of!(InitConf, sidi)]
/// .copy_from_slice(&[1,2,3,4]);
///
/// // Conversion from bytes
/// let ih = Ref::<&mut [u8], Envelope<InitConf>>::new(ih).unwrap();
///
/// // Check that write above on byte representation was effective
/// assert_eq!(ih.payload.sidi, [1,2,3,4]);
/// ```
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
#[derive(AsBytes, FromBytes, FromZeroes, Debug)]
pub struct InitConf {
/// Copied from InitHello
pub sidi: [u8; 4],
@@ -82,8 +239,53 @@ pub struct InitConf {
pub auth: [u8; aead::TAG_LEN],
}
/// This is the fourth message sent by the initiator to the responder
/// during the execution of the Rosenpass protocol in response to [RespHello].
///
/// When transmitted on the wire, this type will generally be wrapped into [Envelope].
///
/// This message does not serve a cryptographic purpose; it just tells the initiator
/// to stop package retransmission.
///
/// This message should really be called `RespConf`, but when we wrote the protocol,
/// we initially designed the protocol we still though Rosenpass itself should do
/// payload transmission at some point so `EmptyData` could have served as a more generic
/// mechanism.
///
/// We might add payload transmission in the future again, but we will treat
/// it as a protocol extension if we do.
///
/// # Examples
///
/// Check out the code of [crate::protocol::CryptoServer::handle_init_conf] (generation on
/// responder side) and [crate::protocol::CryptoServer::handle_resp_conf] (processing on
/// initiator side) to understand how this is used.
///
/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate.
///
/// ```
/// use rosenpass::msgs::{Envelope, EmptyData};
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
/// use memoffset::span_of;
///
/// // Zero initialization
/// let mut ih = Envelope::<EmptyData>::new_zeroed();
///
/// // Conversion to byte representation
/// let ih = ih.as_bytes_mut();
///
/// // Set value on byte representation
/// ih[span_of!(Envelope<EmptyData>, payload)][span_of!(EmptyData, sid)]
/// .copy_from_slice(&[1,2,3,4]);
///
/// // Conversion from bytes
/// let ih = Ref::<&mut [u8], Envelope<EmptyData>>::new(ih).unwrap();
///
/// // Check that write above on byte representation was effective
/// assert_eq!(ih.payload.sid, [1,2,3,4]);
/// ```
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
#[derive(AsBytes, FromBytes, FromZeroes, Clone, Copy)]
pub struct EmptyData {
/// Copied from RespHello
pub sid: [u8; 4],
@@ -93,6 +295,22 @@ pub struct EmptyData {
pub auth: [u8; aead::TAG_LEN],
}
/// Cookie encrypted and sent to the initiator by the responder in [RespHello]
/// and returned by the initiator in [InitConf].
///
/// The encryption key is randomly chosen by the responder and frequently regenerated.
/// Using this biscuit value in the protocol allows us to make sure that the responder
/// is mostly stateless until full initiator authentication is achieved, which is needed
/// to prevent denial of service attacks. See the [whitepaper](https://rosenpass.eu/whitepaper.pdf)
/// ([/papers/whitepaper.md] in this repository).
///
/// # Examples
///
/// To understand how the biscuit is used, it is best to read
/// the code of [crate::protocol::HandshakeState::store_biscuit] and
/// [crate::protocol::HandshakeState::load_biscuit]
///
/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate.
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct Biscuit {
@@ -104,12 +322,20 @@ pub struct Biscuit {
pub ck: [u8; KEY_LEN],
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct DataMsg {
pub dummy: [u8; 4],
}
/// Specialized message for use in the cookie mechanism.
///
/// See the [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository) for details.
///
/// Generally used together with [CookieReply] which brings this up to the size
/// of [InitHello] to avoid amplification Denial of Service attacks.
///
/// # Examples
///
/// To understand how the biscuit is used, it is best to read
/// the code of [crate::protocol::CryptoServer::handle_cookie_reply] and
/// [crate::protocol::CryptoServer::handle_msg_under_load].
///
/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate.
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct CookieReplyInner {
@@ -123,6 +349,20 @@ pub struct CookieReplyInner {
pub cookie_encrypted: [u8; xaead::NONCE_LEN + COOKIE_SIZE + xaead::TAG_LEN],
}
/// Specialized message for use in the cookie mechanism.
///
/// This just brings [CookieReplyInner] up to the size
/// of [InitHello] to avoid amplification Denial of Service attacks.
///
/// See the [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository) for details.
///
/// # Examples
///
/// To understand how the biscuit is used, it is best to read
/// the code of [crate::protocol::CryptoServer::handle_cookie_reply] and
/// [crate::protocol::CryptoServer::handle_msg_under_load].
///
/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate.
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct CookieReply {
@@ -130,33 +370,46 @@ pub struct CookieReply {
pub padding: [u8; size_of::<Envelope<InitHello>>() - size_of::<CookieReplyInner>()],
}
// Traits /////////////////////////////////////////////////////////////////////
pub trait WireMsg: std::fmt::Debug {
const MSG_TYPE: MsgType;
const MSG_TYPE_U8: u8 = Self::MSG_TYPE as u8;
const BYTES: usize;
}
// Constants //////////////////////////////////////////////////////////////////
pub const SESSION_ID_LEN: usize = 4;
pub const BISCUIT_ID_LEN: usize = 12;
pub const WIRE_ENVELOPE_LEN: usize = 1 + 3 + 16 + 16; // TODO verify this
/// Size required to fit any message in binary form
pub const MAX_MESSAGE_LEN: usize = 2500; // TODO fix this
/// Recognized message types
///
/// # Examples
///
/// ```
/// use rosenpass::msgs::MsgType;
/// use rosenpass::msgs::MsgType as M;
///
/// let values = [M::InitHello, M::RespHello, M::InitConf, M::EmptyData, M::CookieReply];
/// let values_u8 = values.map(|v| -> u8 { v.into() });
///
/// // Can be converted to and from u8 using [::std::convert::Into] or [::std::convert::From]
/// for v in values.iter().copied() {
/// let v_u8 : u8 = v.into();
/// let v2 : MsgType = v_u8.try_into()?;
/// assert_eq!(v, v2);
/// }
///
/// // Converting an unsupported type produces an error
/// let invalid_values = (u8::MIN..=u8::MAX)
/// .filter(|v| !values_u8.contains(v));
/// for v in invalid_values {
/// let res : Result<MsgType, _> = v.try_into();
/// assert!(res.is_err());
/// }
///
/// Ok::<(), anyhow::Error>(())
/// ```
#[repr(u8)]
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
pub enum MsgType {
/// MsgType for [InitHello]
InitHello = 0x81,
/// MsgType for [RespHello]
RespHello = 0x82,
/// MsgType for [InitConf]
InitConf = 0x83,
/// MsgType for [EmptyData]
EmptyData = 0x84,
DataMsg = 0x85,
/// MsgType for [CookieReply]
CookieReply = 0x86,
}
@@ -169,7 +422,6 @@ impl TryFrom<u8> for MsgType {
0x82 => MsgType::RespHello,
0x83 => MsgType::InitConf,
0x84 => MsgType::EmptyData,
0x85 => MsgType::DataMsg,
0x86 => MsgType::CookieReply,
_ => return Err(RosenpassError::InvalidMessageType(value)),
})
@@ -182,12 +434,6 @@ impl From<MsgType> for u8 {
}
}
/// length in bytes of an unencrypted Biscuit (plain text)
pub const BISCUIT_PT_LEN: usize = size_of::<Biscuit>();
/// Length in bytes of an encrypted Biscuit (cipher text)
pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + xaead::NONCE_LEN + xaead::TAG_LEN;
#[cfg(test)]
mod test_constants {
use crate::msgs::{BISCUIT_CT_LEN, BISCUIT_PT_LEN};

View File

@@ -1,3 +1,75 @@
//! Module containing the cryptographic protocol implementation
//!
//! # Overview
//!
//! The most important types in this module probably are [PollResult]
//! & [CryptoServer]. Once a [CryptoServer] is created, the server is
//! provided with new messages via the [CryptoServer::handle_msg] method.
//! The [CryptoServer::poll] method can be used to let the server work, which
//! will eventually yield a [PollResult]. Said [PollResult] contains
//! prescriptive activities to be carried out. [CryptoServer::osk] can than
//! be used to extract the shared key for two peers, once a key-exchange was
//! successful.
//!
//! TODO explain briefly the role of epki
//!
//! # Example Handshake
//!
//! This example illustrates a minimal setup for a key-exchange between two
//! [CryptoServer].
//!
//! ```
//! use std::ops::DerefMut;
//! use rosenpass_secret_memory::policy::*;
//! use rosenpass_cipher_traits::Kem;
//! use rosenpass_ciphers::kem::StaticKem;
//! use rosenpass::{
//! protocol::{SSk, SPk, MsgBuf, PeerPtr, CryptoServer, SymKey},
//! };
//! # fn main() -> anyhow::Result<()> {
//! // Set security policy for storing secrets
//!
//! secret_policy_try_use_memfd_secrets();
//!
//! // initialize secret and public key for peer a ...
//! let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero());
//! StaticKem::keygen(peer_a_sk.secret_mut(), peer_a_pk.deref_mut())?;
//!
//! // ... and for peer b
//! let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero());
//! StaticKem::keygen(peer_b_sk.secret_mut(), peer_b_pk.deref_mut())?;
//!
//! // initialize server and a pre-shared key
//! let psk = SymKey::random();
//! let mut a = CryptoServer::new(peer_a_sk, peer_a_pk.clone());
//! let mut b = CryptoServer::new(peer_b_sk, peer_b_pk.clone());
//!
//! // introduce peers to each other
//! a.add_peer(Some(psk.clone()), peer_b_pk)?;
//! b.add_peer(Some(psk), peer_a_pk)?;
//!
//! // declare buffers for message exchange
//! let (mut a_buf, mut b_buf) = (MsgBuf::zero(), MsgBuf::zero());
//!
//! // let a initiate a handshake
//! let mut maybe_len = Some(a.initiate_handshake(PeerPtr(0), a_buf.as_mut_slice())?);
//!
//! // let a and b communicate
//! while let Some(len) = maybe_len {
//! maybe_len = b.handle_msg(&a_buf[..len], &mut b_buf[..])?.resp;
//! std::mem::swap(&mut a, &mut b);
//! std::mem::swap(&mut a_buf, &mut b_buf);
//! }
//!
//! // all done! Extract the shared keys and ensure they are identical
//! let a_key = a.osk(PeerPtr(0))?;
//! let b_key = b.osk(PeerPtr(0))?;
//! assert_eq!(a_key.secret(), b_key.secret(),
//! "the key exchanged failed to establish a shared secret");
//! # Ok(())
//! # }
//! ```
mod build_crypto_server;
#[allow(clippy::module_inception)]
mod protocol;

View File

@@ -1,76 +1,6 @@
//! Module containing the cryptographic protocol implementation
//!
//! # Overview
//!
//! The most important types in this module probably are [PollResult]
//! & [CryptoServer]. Once a [CryptoServer] is created, the server is
//! provided with new messages via the [CryptoServer::handle_msg] method.
//! The [CryptoServer::poll] method can be used to let the server work, which
//! will eventually yield a [PollResult]. Said [PollResult] contains
//! prescriptive activities to be carried out. [CryptoServer::osk] can than
//! be used to extract the shared key for two peers, once a key-exchange was
//! successful.
//!
//! TODO explain briefly the role of epki
//!
//! # Example Handshake
//!
//! This example illustrates a minimal setup for a key-exchange between two
//! [CryptoServer].
//!
//! ```
//! use std::ops::DerefMut;
//! use rosenpass_secret_memory::policy::*;
//! use rosenpass_cipher_traits::Kem;
//! use rosenpass_ciphers::kem::StaticKem;
//! use rosenpass::{
//! protocol::{SSk, SPk, MsgBuf, PeerPtr, CryptoServer, SymKey},
//! };
//! # fn main() -> anyhow::Result<()> {
//! // Set security policy for storing secrets
//!
//! secret_policy_try_use_memfd_secrets();
//!
//! // initialize secret and public key for peer a ...
//! let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero());
//! StaticKem::keygen(peer_a_sk.secret_mut(), peer_a_pk.deref_mut())?;
//!
//! // ... and for peer b
//! let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero());
//! StaticKem::keygen(peer_b_sk.secret_mut(), peer_b_pk.deref_mut())?;
//!
//! // initialize server and a pre-shared key
//! let psk = SymKey::random();
//! let mut a = CryptoServer::new(peer_a_sk, peer_a_pk.clone());
//! let mut b = CryptoServer::new(peer_b_sk, peer_b_pk.clone());
//!
//! // introduce peers to each other
//! a.add_peer(Some(psk.clone()), peer_b_pk)?;
//! b.add_peer(Some(psk), peer_a_pk)?;
//!
//! // declare buffers for message exchange
//! let (mut a_buf, mut b_buf) = (MsgBuf::zero(), MsgBuf::zero());
//!
//! // let a initiate a handshake
//! let mut maybe_len = Some(a.initiate_handshake(PeerPtr(0), a_buf.as_mut_slice())?);
//!
//! // let a and b communicate
//! while let Some(len) = maybe_len {
//! maybe_len = b.handle_msg(&a_buf[..len], &mut b_buf[..])?.resp;
//! std::mem::swap(&mut a, &mut b);
//! std::mem::swap(&mut a_buf, &mut b_buf);
//! }
//!
//! // all done! Extract the shared keys and ensure they are identical
//! let a_key = a.osk(PeerPtr(0))?;
//! let b_key = b.osk(PeerPtr(0))?;
//! assert_eq!(a_key.secret(), b_key.secret(),
//! "the key exchanged failed to establish a shared secret");
//! # Ok(())
//! # }
//! ```
use std::borrow::Borrow;
use std::convert::Infallible;
use std::fmt::Debug;
use std::mem::size_of;
use std::ops::Deref;
use std::{
@@ -88,9 +18,14 @@ use memoffset::span_of;
use rosenpass_cipher_traits::Kem;
use rosenpass_ciphers::hash_domain::{SecretHashDomain, SecretHashDomainNamespace};
use rosenpass_ciphers::kem::{EphemeralKem, StaticKem};
use rosenpass_ciphers::keyed_hash;
use rosenpass_ciphers::{aead, xaead, KEY_LEN};
use rosenpass_constant_time as constant_time;
use rosenpass_secret_memory::{Public, PublicBox, Secret};
use rosenpass_to::ops::copy_slice;
use rosenpass_to::To;
use rosenpass_util::functional::ApplyExt;
use rosenpass_util::mem::DiscardResultExt;
use rosenpass_util::{cat, mem::cpy_min, time::Timebase};
use zerocopy::{AsBytes, FromBytes, Ref};
@@ -200,6 +135,7 @@ pub struct CryptoServer {
// Peer/Handshake DB
pub peers: Vec<Peer>,
pub index: HashMap<IndexKey, PeerNo>,
pub known_response_hasher: KnownResponseHasher,
// Tick handling
pub peer_poll_off: usize,
@@ -229,6 +165,7 @@ pub type BiscuitKey = CookieStore<KEY_LEN>;
pub enum IndexKey {
Peer(PeerId),
Sid(SessionId),
KnownInitConfResponse(KnownResponseHash),
}
#[derive(Debug)]
@@ -239,6 +176,7 @@ pub struct Peer {
pub session: Option<Session>,
pub handshake: Option<InitiatorHandshake>,
pub initiation_requested: bool,
pub known_init_conf_response: Option<KnownInitConfResponse>,
}
impl Peer {
@@ -250,6 +188,7 @@ impl Peer {
session: None,
initiation_requested: false,
handshake: None,
known_init_conf_response: None,
}
}
}
@@ -308,6 +247,50 @@ pub struct InitiatorHandshake {
pub cookie_value: CookieStore<COOKIE_VALUE_LEN>,
}
pub struct KnownResponse<ResponseType: AsBytes + FromBytes> {
received_at: Timing,
request_mac: KnownResponseHash,
response: Envelope<ResponseType>,
}
impl<ResponseType: AsBytes + FromBytes> Debug for KnownResponse<ResponseType> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KnownResponse")
.field("received_at", &self.received_at)
.field("request_mac", &self.request_mac)
.field("response", &"...")
.finish()
}
}
pub type KnownInitConfResponse = KnownResponse<EmptyData>;
pub type KnownResponseHash = Public<16>;
#[derive(Debug)]
pub struct KnownResponseHasher {
pub key: SymKey,
}
impl KnownResponseHasher {
fn new() -> Self {
Self {
key: SymKey::random(),
}
}
/// # Panic & Safety
///
/// Panics in case of a problem with this underlying hash function
pub fn hash<Msg: AsBytes + FromBytes>(&self, msg: &Envelope<Msg>) -> KnownResponseHash {
let data = &msg.as_bytes()[span_of!(Envelope<Msg>, msg_type..cookie)];
let hash = keyed_hash::hash(self.key.secret(), data)
.to_this(Public::<32>::zero)
.unwrap();
Public::from_slice(&hash[0..16]) // truncate to 16 bytes
}
}
#[derive(Debug)]
pub struct Session {
// Metadata
@@ -369,6 +352,12 @@ pub struct IniHsPtr(pub usize);
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct SessionPtr(pub usize);
/// Valid index to [CryptoServer::peers] cookie value
pub struct PeerCookieValuePtr(usize); // TODO: Change
/// Valid index to [CryptoServer::peers] known init conf response
pub struct KnownInitConfResponsePtr(PeerNo);
/// Valid index to [CryptoServer::biscuit_keys]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct BiscuitKeyPtr(pub usize);
@@ -377,14 +366,17 @@ pub struct BiscuitKeyPtr(pub usize);
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct ServerCookieSecretPtr(pub usize);
/// Valid index to [CryptoServer::peers] cookie value
pub struct PeerCookieValuePtr(usize);
impl PeerPtr {
/// # Panic & Safety
///
/// The function panics if the peer referenced by this PeerPtr does not exist.
pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a Peer {
&srv.peers[self.0]
}
/// # Panic & Safety
///
/// The function panics if the peer referenced by this PeerPtr does not exist.
pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut Peer {
&mut srv.peers[self.0]
}
@@ -400,6 +392,10 @@ impl PeerPtr {
pub fn cv(&self) -> PeerCookieValuePtr {
PeerCookieValuePtr(self.0)
}
pub fn known_init_conf_response(&self) -> KnownInitConfResponsePtr {
KnownInitConfResponsePtr(self.0)
}
}
impl IniHsPtr {
@@ -508,6 +504,118 @@ impl PeerCookieValuePtr {
}
}
impl KnownInitConfResponsePtr {
pub fn peer(&self) -> PeerPtr {
PeerPtr(self.0)
}
/// # Panic & Safety
///
/// The function panics if the peer referenced by this KnownInitConfResponsePtr does not exist.
pub fn get<'a>(&self, srv: &'a CryptoServer) -> Option<&'a KnownInitConfResponse> {
self.peer().get(srv).known_init_conf_response.as_ref()
}
/// # Panic & Safety
///
/// The function panics if the peer referenced by this KnownInitConfResponsePtr does not exist.
pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> Option<&'a mut KnownInitConfResponse> {
self.peer().get_mut(srv).known_init_conf_response.as_mut()
}
/// # Panic & Safety
///
/// The function panics if
///
/// - the peer referenced by this KnownInitConfResponsePtr does not exist
/// - the peer contains a KnownInitConfResponse (i.e. if [Peer::known_init_conf_response] is Some(...)), but the index to this KnownInitConfResponsePtr is missing (i.e. there is no appropriate index
/// value in [CryptoServer::index])
pub fn remove(&self, srv: &mut CryptoServer) -> Option<KnownInitConfResponse> {
let peer = self.peer();
let val = peer.get_mut(srv).known_init_conf_response.take()?;
let lookup_key = IndexKey::KnownInitConfResponse(val.request_mac);
srv.index.remove(&lookup_key).unwrap();
Some(val)
}
pub fn insert(&self, srv: &mut CryptoServer, known_response: KnownInitConfResponse) {
self.remove(srv).discard_result();
let index_key = IndexKey::KnownInitConfResponse(known_response.request_mac);
self.peer().get_mut(srv).known_init_conf_response = Some(known_response);
// There is a question here whether we should just discard the result…or panic if the
// result is Some(...).
//
// The result being anything other than None should never occur:
// - If we have never seen this InitConf message, then the result should be None and no value should
// have been written. This is fine.
// - If we have seen this message before, we should have responded with a known answer
// which would be fine
// - If we have never seen this InitConf message before, but the hashes are the same, this
// would constitute a collision on our hash function, which is security because the
// cryptography (collision resistance of our hash) prevents this. If this happened, it
// would be bad but we could not detect it.
if srv.index.insert(index_key, self.0).is_some() {
log::warn!(
r#"
Replaced a cached message in the InitConf known-response table
for network retransmission handling. This should never happen and is
probably a bug. Please report seeing this message at the following location:
https://github.com/rosenpass/rosenpass/issues
"#
);
}
}
pub fn lookup_for_request_msg(
srv: &CryptoServer,
req: &Envelope<InitConf>,
) -> Option<KnownInitConfResponsePtr> {
let index_key = Self::index_key_for_msg(srv, req);
let peer_no = *srv.index.get(&index_key)?;
Some(Self(peer_no))
}
pub fn lookup_response_for_request_msg<'a>(
srv: &'a CryptoServer,
req: &Envelope<InitConf>,
) -> Option<&'a Envelope<EmptyData>> {
Self::lookup_for_request_msg(srv, req)?
.get(srv)
.map(|v| &v.response)
}
pub fn insert_for_request_msg(
srv: &mut CryptoServer,
peer: PeerPtr,
req: &Envelope<InitConf>,
res: Envelope<EmptyData>,
) {
let ptr = peer.known_init_conf_response();
ptr.insert(
srv,
KnownInitConfResponse {
received_at: srv.timebase.now(),
request_mac: Self::index_key_hash_for_msg(srv, req),
response: res,
},
);
}
pub fn index_key_hash_for_msg(
srv: &CryptoServer,
req: &Envelope<InitConf>,
) -> KnownResponseHash {
srv.known_response_hasher.hash(req)
}
pub fn index_key_for_msg(srv: &CryptoServer, req: &Envelope<InitConf>) -> IndexKey {
Self::index_key_hash_for_msg(srv, req).apply(IndexKey::KnownInitConfResponse)
}
}
// DATABASE //////////////////////////////////////
impl CryptoServer {
@@ -525,6 +633,7 @@ impl CryptoServer {
biscuit_keys: [CookieStore::new(), CookieStore::new()],
peers: Vec::new(),
index: HashMap::new(),
known_response_hasher: KnownResponseHasher::new(),
peer_poll_off: 0,
cookie_secrets: [CookieStore::new(), CookieStore::new()],
}
@@ -563,6 +672,7 @@ impl CryptoServer {
biscuit_used: BiscuitId::zero(),
session: None,
handshake: None,
known_init_conf_response: None,
initiation_requested: false,
};
let peerid = peer.pidt()?;
@@ -695,6 +805,7 @@ impl Peer {
biscuit_used: BiscuitId::zero(),
session: None,
handshake: None,
known_init_conf_response: None,
initiation_requested: false,
}
}
@@ -852,6 +963,25 @@ impl Mortal for PeerCookieValuePtr {
}
}
impl Mortal for KnownInitConfResponsePtr {
fn created_at(&self, srv: &CryptoServer) -> Option<Timing> {
let t = self.get(srv)?.received_at;
if t < 0.0 {
None
} else {
Some(t)
}
}
fn retire_at(&self, srv: &CryptoServer) -> Option<Timing> {
self.die_at(srv)
}
fn die_at(&self, srv: &CryptoServer) -> Option<Timing> {
self.created_at(srv).map(|t| t + REKEY_AFTER_TIME_RESPONDER)
}
}
/// Trait extension to the [Mortal] Trait, that enables nicer access to timing
/// information
trait MortalExt: Mortal {
@@ -1115,10 +1245,38 @@ impl CryptoServer {
ensure!(msg_in.check_seal(self)?, seal_broken);
let mut msg_out = truncating_cast_into::<Envelope<EmptyData>>(tx_buf)?;
let (peer, if_exchanged) =
self.handle_init_conf(&msg_in.payload, &mut msg_out.payload)?;
// Check if we have a cached response
let peer = match KnownInitConfResponsePtr::lookup_for_request_msg(self, &msg_in) {
// Cached response; copy out of cache
Some(cached) => {
let peer = cached.peer();
let cached = cached
.get(self)
.map(|v| v.response.borrow())
// Invalid! Found peer no with cache in index but the cache does not exist
.unwrap();
copy_slice(cached.as_bytes()).to(msg_out.as_bytes_mut());
peer
}
// No cached response, actually call cryptographic handler
None => {
let peer = self.handle_init_conf(&msg_in.payload, &mut msg_out.payload)?;
KnownInitConfResponsePtr::insert_for_request_msg(
self,
peer,
&msg_in,
msg_out.clone(),
);
exchanged = true;
peer
}
};
len = self.seal_and_commit_msg(peer, MsgType::EmptyData, &mut msg_out)?;
exchanged = if_exchanged;
peer
}
Ok(MsgType::EmptyData) => {
@@ -1128,7 +1286,6 @@ impl CryptoServer {
self.handle_resp_conf(&msg_in.payload)?
}
Ok(MsgType::DataMsg) => bail!("DataMsg handling not implemented!"),
Ok(MsgType::CookieReply) => {
let msg_in: Ref<&[u8], CookieReply> =
Ref::new(rx_buf).ok_or(RosenpassError::BufferSizeMismatch)?;
@@ -1402,7 +1559,8 @@ impl Pollable for PeerPtr {
PollResult::SendInitiation(*self)
},
)
.poll_child(srv, &hs) // Defer to the handshake for polling (retransmissions)
.poll_child(srv, &hs)? // Defer to the handshake for polling (retransmissions)
.poll_child(srv, &self.known_init_conf_response())
}
}
@@ -1417,6 +1575,15 @@ impl Pollable for IniHsPtr {
}
}
impl Pollable for KnownInitConfResponsePtr {
fn poll(&self, srv: &mut CryptoServer) -> Result<PollResult> {
begin_poll()
// Erase stale cache
.sched(self.life_left(srv), void_poll(|| self.remove(srv)))
.ok()
}
}
// MESSAGE RETRANSMISSION ////////////////////////
impl CryptoServer {
@@ -1701,15 +1868,6 @@ impl HandshakeState {
.find_peer(pid) // TODO: FindPeer should return a Result<()>
.with_context(|| format!("Could not decode biscuit for peer {pid:?}: No such peer."))?;
// Defense against replay attacks; implementations may accept
// the most recent biscuit no again (bn = peer.bn_{prev}) which
// indicates retransmission
// TODO: Handle retransmissions without involving the crypto code
ensure!(
constant_time::compare(&biscuit.biscuit_no, &*peer.get(srv).biscuit_used) >= 0,
"Rejecting biscuit: Outdated biscuit number"
);
Ok((peer, no, hs))
}
@@ -1947,12 +2105,7 @@ impl CryptoServer {
Ok(peer)
}
pub fn handle_init_conf(
&mut self,
ic: &InitConf,
rc: &mut EmptyData,
) -> Result<(PeerPtr, bool)> {
let mut exchanged = false;
pub fn handle_init_conf(&mut self, ic: &InitConf, rc: &mut EmptyData) -> Result<PeerPtr> {
// (peer, bn) ← LoadBiscuit(InitConf.biscuit)
// ICR1
let (peer, biscuit_no, mut core) = HandshakeState::load_biscuit(
@@ -1972,20 +2125,23 @@ impl CryptoServer {
core.decrypt_and_mix(&mut [0u8; 0], &ic.auth)?;
// ICR5
if constant_time::compare(&*biscuit_no, &*peer.get(self).biscuit_used) > 0 {
// ICR6
peer.get_mut(self).biscuit_used = biscuit_no;
// Defense against replay attacks; implementations may accept
// the most recent biscuit no again (bn = peer.bn_{prev}) which
// indicates retransmission
ensure!(
constant_time::compare(&*biscuit_no, &*peer.get(self).biscuit_used) > 0,
"Rejecting biscuit: Outdated biscuit number"
);
// ICR7
peer.session()
.insert(self, core.enter_live(self, HandshakeRole::Responder)?)?;
// TODO: This should be part of the protocol specification.
// Abort any ongoing handshake from initiator role
peer.hs().take(self);
// ICR6
peer.get_mut(self).biscuit_used = biscuit_no;
// Only exchange key on new biscuit number- avoid duplicate key exchanges on retransmitted InitConf messages
exchanged = true;
}
// ICR7
peer.session()
.insert(self, core.enter_live(self, HandshakeRole::Responder)?)?;
// TODO: This should be part of the protocol specification.
// Abort any ongoing handshake from initiator role
peer.hs().take(self);
// TODO: Implementing RP should be possible without touching the live session stuff
// TODO: I fear that this may lead to race conditions; the acknowledgement may be
@@ -2023,7 +2179,7 @@ impl CryptoServer {
let k = ses.txkm.secret();
aead::encrypt(&mut rc.auth, k, &n, &[], &[])?; // ct, k, n, ad, pt
Ok((peer, exchanged))
Ok(peer)
}
pub fn handle_resp_conf(&mut self, rc: &EmptyData) -> Result<PeerPtr> {
@@ -2140,10 +2296,11 @@ fn truncating_cast_into_nomut<T: FromBytes>(buf: &[u8]) -> Result<Ref<&[u8], T>,
#[cfg(test)]
mod test {
use std::{net::SocketAddrV4, ops::DerefMut, thread::sleep, time::Duration};
use std::{borrow::BorrowMut, net::SocketAddrV4, ops::DerefMut, thread::sleep, time::Duration};
use super::*;
use serial_test::serial;
use zerocopy::FromZeroes;
struct VecHostIdentifier(Vec<u8>);
@@ -2559,4 +2716,186 @@ mod test {
.is_err());
});
}
#[test]
fn init_conf_retransmission() -> anyhow::Result<()> {
rosenpass_secret_memory::secret_policy_try_use_memfd_secrets();
fn keypair() -> anyhow::Result<(SSk, SPk)> {
let (mut sk, mut pk) = (SSk::zero(), SPk::zero());
StaticKem::keygen(sk.secret_mut(), pk.deref_mut())?;
Ok((sk, pk))
}
fn proc_initiation(
srv: &mut CryptoServer,
peer: PeerPtr,
) -> anyhow::Result<Envelope<InitHello>> {
let mut buf = MsgBuf::zero();
srv.initiate_handshake(peer, buf.as_mut_slice())?
.discard_result();
let msg = truncating_cast_into::<Envelope<InitHello>>(buf.borrow_mut())?;
Ok(msg.read())
}
fn proc_msg<Rx: AsBytes + FromBytes, Tx: AsBytes + FromBytes>(
srv: &mut CryptoServer,
rx: &Envelope<Rx>,
) -> anyhow::Result<Envelope<Tx>> {
let mut buf = MsgBuf::zero();
srv.handle_msg(rx.as_bytes(), buf.as_mut_slice())?
.resp
.context("Failed to produce RespHello message")?
.discard_result();
let msg = truncating_cast_into::<Envelope<Tx>>(buf.borrow_mut())?;
Ok(msg.read())
}
fn proc_init_hello(
srv: &mut CryptoServer,
ih: &Envelope<InitHello>,
) -> anyhow::Result<Envelope<RespHello>> {
proc_msg::<InitHello, RespHello>(srv, ih)
}
fn proc_resp_hello(
srv: &mut CryptoServer,
rh: &Envelope<RespHello>,
) -> anyhow::Result<Envelope<InitConf>> {
proc_msg::<RespHello, InitConf>(srv, rh)
}
fn proc_init_conf(
srv: &mut CryptoServer,
rh: &Envelope<InitConf>,
) -> anyhow::Result<Envelope<EmptyData>> {
proc_msg::<InitConf, EmptyData>(srv, rh)
}
fn poll(srv: &mut CryptoServer) -> anyhow::Result<()> {
// Discard all events; just apply the side effects
while !matches!(srv.poll()?, PollResult::Sleep(_)) {}
Ok(())
}
// TODO: Implement Clone on our message types
fn clone_msg<Msg: AsBytes + FromBytes>(msg: &Msg) -> anyhow::Result<Msg> {
Ok(truncating_cast_into_nomut::<Msg>(msg.as_bytes())?.read())
}
fn break_payload<Msg: AsBytes + FromBytes>(
srv: &mut CryptoServer,
peer: PeerPtr,
msg: &Envelope<Msg>,
) -> anyhow::Result<Envelope<Msg>> {
let mut msg = clone_msg(msg)?;
msg.as_bytes_mut()[memoffset::offset_of!(Envelope<Msg>, payload)] ^= 0x01;
msg.seal(peer, srv)?; // Recalculate seal; we do not want to focus on "seal broken" errs
Ok(msg)
}
fn time_travel_forward(srv: &mut CryptoServer, secs: f64) {
let dur = std::time::Duration::from_secs_f64(secs);
srv.timebase.0 = srv.timebase.0.checked_sub(dur).unwrap();
}
fn check_faulty_proc_init_conf(srv: &mut CryptoServer, ic_broken: &Envelope<InitConf>) {
let mut buf = MsgBuf::zero();
let res = srv.handle_msg(ic_broken.as_bytes(), buf.as_mut_slice());
assert!(res.is_err());
}
fn check_retransmission(
srv: &mut CryptoServer,
ic: &Envelope<InitConf>,
ic_broken: &Envelope<InitConf>,
rc: &Envelope<EmptyData>,
) -> anyhow::Result<()> {
// Processing the same RespHello package again leads to retransmission (i.e. exactly the
// same output)
let rc_dup = proc_init_conf(srv, ic)?;
assert_eq!(rc.as_bytes(), rc_dup.as_bytes());
// Though if we directly call handle_resp_hello() we get an error since
// retransmission is not being handled by the cryptographic code
let mut discard_resp_conf = EmptyData::new_zeroed();
let res = srv.handle_init_conf(&ic.payload, &mut discard_resp_conf);
assert!(res.is_err());
// Obviously, a broken InitConf message should still be rejected
check_faulty_proc_init_conf(srv, ic_broken);
Ok(())
}
let (ska, pka) = keypair()?;
let (skb, pkb) = keypair()?;
// initialize server and a pre-shared key
let mut a = CryptoServer::new(ska, pka.clone());
let mut b = CryptoServer::new(skb, pkb.clone());
// introduce peers to each other
let b_peer = a.add_peer(None, pkb)?;
let a_peer = b.add_peer(None, pka)?;
// Execute protocol up till the responder confirmation (EmptyData)
let ih1 = proc_initiation(&mut a, b_peer)?;
let rh1 = proc_init_hello(&mut b, &ih1)?;
let ic1 = proc_resp_hello(&mut a, &rh1)?;
let rc1 = proc_init_conf(&mut b, &ic1)?;
// Modified version of ic1 and rc1, for tests that require it
let ic1_broken = break_payload(&mut a, b_peer, &ic1)?;
assert_ne!(ic1.as_bytes(), ic1_broken.as_bytes());
// Modified version of rc1, for tests that require it
let rc1_broken = break_payload(&mut b, a_peer, &rc1)?;
assert_ne!(rc1.as_bytes(), rc1_broken.as_bytes());
// Retransmission works as designed
check_retransmission(&mut b, &ic1, &ic1_broken, &rc1)?;
// Even with a couple of poll operations in between (which clears the cache
// after a time out of two minutes…we should never hit this time out in this
// cache)
for _ in 0..4 {
poll(&mut b)?;
check_retransmission(&mut b, &ic1, &ic1_broken, &rc1)?;
}
// We can even validate that the data is coming out of the cache by changing the cache
// to use our broken messages. It does not matter that these messages are cryptographically
// broken since we insert them manually into the cache
// a_peer.known_init_conf_response()
KnownInitConfResponsePtr::insert_for_request_msg(
&mut b,
a_peer,
&ic1_broken,
rc1_broken.clone(),
);
check_retransmission(&mut b, &ic1_broken, &ic1, &rc1_broken)?;
// Lets reset to the correct message though
KnownInitConfResponsePtr::insert_for_request_msg(&mut b, a_peer, &ic1, rc1.clone());
// Again, nothing changes after calling poll
poll(&mut b)?;
check_retransmission(&mut b, &ic1, &ic1_broken, &rc1)?;
// Except if we jump forward into the future past the point where the responder
// starts to initiate rekeying; in this case, the automatic time out is triggered and the cache is cleared
time_travel_forward(&mut b, REKEY_AFTER_TIME_RESPONDER);
// As long as we do not call poll, everything is fine
check_retransmission(&mut b, &ic1, &ic1_broken, &rc1)?;
// But after we do, the response is gone and can not be recreated
// since the biscuit is stale
poll(&mut b)?;
check_faulty_proc_init_conf(&mut b, &ic1); // ic1 is now effectively broken
assert!(b.peers[0].known_init_conf_response.is_none()); // The cache is gone
Ok(())
}
}

View File

@@ -33,8 +33,10 @@ struct KillChild(std::process::Child);
impl Drop for KillChild {
fn drop(&mut self) {
self.0.kill().discard_result();
self.0.wait().discard_result()
use rustix::process::{kill_process, Pid, Signal::Term};
let pid = Pid::from_child(&self.0);
rustix::process::kill_process(pid, Term).discard_result();
self.0.wait().discard_result();
}
}
@@ -153,7 +155,6 @@ fn api_integration_api_setup() -> anyhow::Result<()> {
peer_b.config_file_path.to_str().context("")?,
])
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn()?,
);

View File

@@ -0,0 +1,99 @@
use rosenpass_util::functional::ApplyExt;
fn expect_section(manpage: &str, section: &str) -> anyhow::Result<()> {
anyhow::ensure!(manpage.lines().any(|line| { line.starts_with(section) }));
Ok(())
}
fn expect_sections(manpage: &str, sections: &[&str]) -> anyhow::Result<()> {
for section in sections.iter().copied() {
expect_section(manpage, section)?;
}
Ok(())
}
fn expect_contents(manpage: &str, patterns: &[&str]) -> anyhow::Result<()> {
for pat in patterns.iter().copied() {
anyhow::ensure!(manpage.contains(pat))
}
Ok(())
}
fn filter_backspace(str: &str) -> anyhow::Result<String> {
let mut out = String::new();
for chr in str.chars() {
if chr == '\x08' {
anyhow::ensure!(out.pop().is_some());
} else {
out.push(chr);
}
}
Ok(out)
}
/// Spot tests about man page generation; these are by far not exhaustive.
#[test]
fn main_fn_generates_manpages() -> anyhow::Result<()> {
let dir = tempfile::TempDir::with_prefix("rosenpass-test-main-fn-generates-mangapges")?;
let cmd_out = test_bin::get_test_bin("rosenpass")
.args(["--generate-manpage", dir.path().to_str().unwrap()])
.output()?;
assert!(cmd_out.status.success());
let expected_manpages = [
"rosenpass.1",
"rosenpass-exchange.1",
"rosenpass-exchange-config.1",
"rosenpass-gen-config.1",
"rosenpass-gen-keys.1",
"rosenpass-keygen.1",
"rosenpass-validate.1",
];
let man_texts: std::collections::HashMap<&str, String> = expected_manpages
.iter()
.copied()
.map(|name| (name, dir.path().join(name)))
.map(|(name, path)| {
let res = std::process::Command::new("man").arg(path).output()?;
assert!(res.status.success());
let body = res
.stdout
.apply(String::from_utf8)?
.apply(|s| filter_backspace(&s))?;
Ok((name, body))
})
.collect::<anyhow::Result<_>>()?;
for (name, body) in man_texts.iter() {
expect_sections(body, &["NAME", "SYNOPSIS", "OPTIONS"])?;
if *name != "rosenpass.1" {
expect_section(body, "DESCRIPTION")?;
}
}
{
let body = man_texts.get("rosenpass.1").unwrap();
expect_sections(
body,
&["EXIT STATUS", "SEE ALSO", "STANDARDS", "AUTHORS", "BUGS"],
)?;
expect_contents(
body,
&[
"[--log-level]",
"rosenpass-exchange-config(1)",
"Start Rosenpass key exchanges based on a configuration file",
"https://rosenpass.eu/whitepaper.pdf",
],
)?;
}
{
let body = man_texts.get("rosenpass-exchange.1").unwrap();
expect_contents(body, &["[-c|--config-file]", "PSK := preshared-key"])?;
}
Ok(())
}

View File

@@ -0,0 +1,10 @@
#[test]
fn main_fn_prints_errors() -> anyhow::Result<()> {
let out = test_bin::get_test_bin("rosenpass")
.args(["exchange-config", "/"])
.output()?;
assert!(!out.status.success());
assert!(String::from_utf8(out.stderr)?.contains("Is a directory (os error 21)"));
Ok(())
}

View File

@@ -26,10 +26,11 @@ rosenpass-wireguard-broker = { workspace = true }
tokio = { workspace = true }
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
ctrlc-async = "3.2"
futures = "0.3"
futures-util = "0.3"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
ctrlc-async = "3.2"
genetlink = "0.2"
rtnetlink = "0.14"
netlink-packet-core = "0.7"

View File

@@ -153,7 +153,7 @@ impl ExchangeOptions {
if let Some(ip) = args.next() {
options.ip = Some(ip);
} else {
return fatal("is option requires parameter", Some(CommandType::Exchange));
return fatal("ip option requires parameter", Some(CommandType::Exchange));
}
}
"listen" => {

View File

@@ -39,7 +39,7 @@ let
}];
};
client_config = {
listen = [ "0.0.0.0:9999" ]; # TODO: Should not be necessary to set, but wouldn't parse.
listen = [ ];
public_key = "/etc/rosenpass/rp0/pqpk";
secret_key = "/run/credentials/rosenpass@rp0.service/pqsk";
verbosity = "Verbose";

View File

@@ -17,7 +17,7 @@ use std::time::Instant;
/// ```
#[derive(Clone, Debug)]
pub struct Timebase(Instant);
pub struct Timebase(pub Instant);
impl Default for Timebase {
// TODO: Implement new()?