mirror of
https://github.com/rosenpass/rosenpass.git
synced 2025-12-05 20:40:02 -08:00
add Rosenpass, the tool
Initial implementation of the Rosenpass tool, implemented by @koraa. Includes contributions and some lints from @wucke13. Co-authored-by: wucke13 <wucke13@gmail.com>
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Rust
|
||||||
|
/target
|
||||||
1449
Cargo.lock
generated
Normal file
1449
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
Normal file
31
Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "rosenpass"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Karolin Varner <karo@cupdev.net>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "handshake"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { version = "1.0.52", features = ["backtrace"] }
|
||||||
|
base64 = "0.13.0"
|
||||||
|
clap = { version = "3.0.0", features = ["yaml"] }
|
||||||
|
static_assertions = "1.1.0"
|
||||||
|
memoffset = "0.6.5"
|
||||||
|
libsodium-sys-stable = { version = "1.19.26", features = ["use-pkg-config"] }
|
||||||
|
oqs-sys = { version = "0.7.1", default-features = false, features = ['classic_mceliece', 'kyber'] }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
thiserror = "1.0.38"
|
||||||
|
paste = "1.0.11"
|
||||||
|
log = { version = "0.4.17", optional = true }
|
||||||
|
env_logger = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = "0.3.5"
|
||||||
|
test_bin = "0.4.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["log", "env_logger"]
|
||||||
176
LICENSE-APACHE
Normal file
176
LICENSE-APACHE
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
23
LICENSE-MIT
Normal file
23
LICENSE-MIT
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
82
benches/handshake.rs
Normal file
82
benches/handshake.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use rosenpass::{
|
||||||
|
pqkem::{CCAKEM, KEM},
|
||||||
|
protocol::{CcaPk, CcaSk, HandleMsgResult, MsgBuf, PeerPtr, Server, SymKey},
|
||||||
|
sodium::sodium_init,
|
||||||
|
};
|
||||||
|
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
fn handle(
|
||||||
|
tx: &mut Server,
|
||||||
|
msgb: &mut MsgBuf,
|
||||||
|
msgl: usize,
|
||||||
|
rx: &mut Server,
|
||||||
|
resb: &mut MsgBuf,
|
||||||
|
) -> Result<(Option<SymKey>, Option<SymKey>)> {
|
||||||
|
let HandleMsgResult {
|
||||||
|
exchanged_with: xch,
|
||||||
|
resp,
|
||||||
|
} = rx.handle_msg(&msgb[..msgl], &mut **resb)?;
|
||||||
|
assert!(matches!(xch, None | Some(PeerPtr(0))));
|
||||||
|
|
||||||
|
let xch = xch.map(|p| rx.osk(p).unwrap());
|
||||||
|
let (rxk, txk) = resp
|
||||||
|
.map(|resl| handle(rx, resb, resl, tx, msgb))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or((None, None));
|
||||||
|
|
||||||
|
assert!(rxk.is_none() || xch.is_none());
|
||||||
|
Ok((txk, rxk.or(xch)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hs(ini: &mut Server, res: &mut Server) -> Result<()> {
|
||||||
|
let (mut inib, mut resb) = (MsgBuf::zero(), MsgBuf::zero());
|
||||||
|
let sz = ini.initiate_handshake(PeerPtr(0), &mut *inib)?;
|
||||||
|
let (kini, kres) = handle(ini, &mut inib, sz, res, &mut resb)?;
|
||||||
|
assert!(kini.unwrap().secret() == kres.unwrap().secret());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keygen() -> Result<(CcaSk, CcaPk)> {
|
||||||
|
let (mut sk, mut pk) = (CcaSk::zero(), CcaPk::zero());
|
||||||
|
CCAKEM::keygen(sk.secret_mut(), pk.secret_mut())?;
|
||||||
|
Ok((sk, pk))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_server_pair() -> Result<(Server, Server)> {
|
||||||
|
let psk = SymKey::random();
|
||||||
|
let ((ska, pka), (skb, pkb)) = (keygen()?, keygen()?);
|
||||||
|
let (mut a, mut b) = (Server::new(ska, pka.clone()), Server::new(skb, pkb.clone()));
|
||||||
|
a.add_peer(Some(psk.clone()), pkb)?;
|
||||||
|
b.add_peer(Some(psk), pka)?;
|
||||||
|
Ok((a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
sodium_init().unwrap();
|
||||||
|
let (mut a, mut b) = make_server_pair().unwrap();
|
||||||
|
c.bench_function("cca_secret_alloc", |bench| {
|
||||||
|
bench.iter(|| {
|
||||||
|
CcaSk::zero();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
c.bench_function("cca_public_alloc", |bench| {
|
||||||
|
bench.iter(|| {
|
||||||
|
CcaPk::zero();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
c.bench_function("keygen", |bench| {
|
||||||
|
bench.iter(|| {
|
||||||
|
keygen().unwrap();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
c.bench_function("handshake", |bench| {
|
||||||
|
bench.iter(|| {
|
||||||
|
hs(black_box(&mut a), black_box(&mut b)).unwrap();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, criterion_benchmark);
|
||||||
|
criterion_main!(benches);
|
||||||
333
rp
Executable file
333
rp
Executable file
@@ -0,0 +1,333 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# String formatting subsystem
|
||||||
|
|
||||||
|
formatting_init() {
|
||||||
|
endl=$'\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
enquote() {
|
||||||
|
while (( $# > 1 )); do
|
||||||
|
printf "%q " "${1}"; shift
|
||||||
|
done
|
||||||
|
if (( $# == 1 )); then
|
||||||
|
printf "%q" "${1}"; shift
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
multiline() {
|
||||||
|
# shellcheck disable=SC1004
|
||||||
|
echo "${1} " | awk '
|
||||||
|
function pm(a, b, l) {
|
||||||
|
return length(a) > l \
|
||||||
|
&& length(b) > l \
|
||||||
|
&& substr(a, 1, l+1) == substr(b, 1, l+1) \
|
||||||
|
? pm(a, b, l+1) : l;
|
||||||
|
}
|
||||||
|
|
||||||
|
!started && $0 !~ /^[ \t]*$/ {
|
||||||
|
started=1
|
||||||
|
match($0, /^[ \t]*/)
|
||||||
|
prefix=substr($0, 1, RLENGTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
started {
|
||||||
|
print(substr($0, 1 + pm($0, prefix)));
|
||||||
|
}
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
dbg() {
|
||||||
|
echo >&2 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup subsystem (sigterm)
|
||||||
|
|
||||||
|
cleanup_init() {
|
||||||
|
cleanup_actions=()
|
||||||
|
trap cleanup_apply exit
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_apply() {
|
||||||
|
local f
|
||||||
|
for f in "${cleanup_actions[@]}"; do
|
||||||
|
eval "${f}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
cleanup_actions+=("$(multiline "${1}")")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Transactional execution subsystem
|
||||||
|
|
||||||
|
frag_init() {
|
||||||
|
explain=0
|
||||||
|
frag_transaction=()
|
||||||
|
frag "
|
||||||
|
#! /bin/bash
|
||||||
|
set -e"
|
||||||
|
}
|
||||||
|
|
||||||
|
frag_apply() {
|
||||||
|
local f
|
||||||
|
for f in "${frag_transaction[@]}"; do
|
||||||
|
if (( explain == 1 )); then
|
||||||
|
dbg "${f}"
|
||||||
|
fi
|
||||||
|
eval "${f}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
frag() {
|
||||||
|
frag_transaction+=("$(multiline "${1}")")
|
||||||
|
}
|
||||||
|
|
||||||
|
frag_append() {
|
||||||
|
local len; len="${#frag_transaction[@]}"
|
||||||
|
frag_transaction=("${frag_transaction[@]:0:len-1}" "${frag_transaction[len-1]}${1}")
|
||||||
|
}
|
||||||
|
|
||||||
|
frag_append_esc() {
|
||||||
|
frag_append " \\${endl}${1}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage documentation subsystem
|
||||||
|
usage_init() {
|
||||||
|
usagestack=("${script}")
|
||||||
|
}
|
||||||
|
|
||||||
|
usage_snap() {
|
||||||
|
echo "${#usagestack}"
|
||||||
|
}
|
||||||
|
|
||||||
|
usage_restore() {
|
||||||
|
local n; n="${1}"
|
||||||
|
dbg REST "${1}"
|
||||||
|
usagestack=("${usagestack[@]:0:n-2}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
dbg "Usage: ${usagestack[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal() {
|
||||||
|
dbg "FATAL: $*"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
genkey() {
|
||||||
|
usagestack+=("PRIVATE_KEYS_DIR")
|
||||||
|
local skdir
|
||||||
|
skdir="${1/\//}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"
|
||||||
|
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
local arg; arg="$1"; shift
|
||||||
|
case "${arg}" in
|
||||||
|
-h | -help | --help | help) usage; return 0 ;;
|
||||||
|
*) fatal "Unknown option ${arg}";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if test -e "${skdir}"; then
|
||||||
|
fatal "PRIVATE_KEYS_DIR \"${skdir}\" already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
frag "
|
||||||
|
umask 077
|
||||||
|
mkdir -p $(enquote "${skdir}")
|
||||||
|
wg genkey > $(enquote "${skdir}"/wgsk)
|
||||||
|
$(enquote "${binary}") keygen \\
|
||||||
|
private-key $(enquote "${skdir}"/pqsk) \\
|
||||||
|
public-key $(enquote "${skdir}"/pqpk)"
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey() {
|
||||||
|
usagestack+=("PRIVATE_KEYS_DIR" "PUBLIC_KEYS_DIR")
|
||||||
|
local skdir pkdir
|
||||||
|
skdir="${1/\//}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"
|
||||||
|
pkdir="${1/\//}"; shift || fatal "Required positional argument: PUBLIC_KEYS_DIR"
|
||||||
|
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
local arg; arg="$1"; shift
|
||||||
|
case "${arg}" in
|
||||||
|
-h | -help | --help | help) usage; exit 0;;
|
||||||
|
*) fatal "Unknown option ${arg}";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if test -e "${pkdir}"; then
|
||||||
|
fatal "PUBLIC_KEYS_DIR \"${pkdir}\" already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
frag "
|
||||||
|
mkdir -p $(enquote "${pkdir}")
|
||||||
|
wg pubkey < $(enquote "${skdir}"/wgsk) > $(enquote "${pkdir}/wgpk")
|
||||||
|
cp $(enquote "${skdir}"/pqpk) $(enquote "${pkdir}/pqpk")"
|
||||||
|
}
|
||||||
|
|
||||||
|
exchange() {
|
||||||
|
usagestack+=("PRIVATE_KEYS_DIR" "[dev <device>]" "[listen <ip>:<port>]" "[peer PUBLIC_KEYS_DIR [endpoint <ip>:<port>] [persistent-keepalive <interval>] [allowed-ips <ip1>/<cidr1>[,<ip2>/<cidr2>]...]]...")
|
||||||
|
local skdir dev lport
|
||||||
|
dev="${project_name}0"
|
||||||
|
skdir="${1/\//}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"
|
||||||
|
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
local arg; arg="$1"; shift
|
||||||
|
case "${arg}" in
|
||||||
|
dev) dev="${1}"; shift || fatal "dev option requires parameter";;
|
||||||
|
peer) set -- "peer" "$@"; break;; # Parsed down below
|
||||||
|
listen)
|
||||||
|
local listen; listen="${1}";
|
||||||
|
lip="${listen%:*}";
|
||||||
|
lport="${listen/*:/}";
|
||||||
|
if [[ "$lip" = "$lport" ]]; then
|
||||||
|
lip="[0::0]"
|
||||||
|
fi
|
||||||
|
shift;;
|
||||||
|
-h | -help | --help | help) usage; return 0;;
|
||||||
|
*) fatal "Unknown option ${arg}";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( $# == 0 )); then
|
||||||
|
fatal "Needs at least one peer specified"
|
||||||
|
fi
|
||||||
|
|
||||||
|
frag "
|
||||||
|
# Create the Wireguard interface
|
||||||
|
ip link add dev $(enquote "${dev}") type wireguard || true"
|
||||||
|
|
||||||
|
cleanup "
|
||||||
|
ip link del dev $(enquote "${dev}") || true"
|
||||||
|
|
||||||
|
frag "
|
||||||
|
ip link set dev $(enquote "${dev}") up"
|
||||||
|
|
||||||
|
frag "
|
||||||
|
# Deploy the classic wireguard private key
|
||||||
|
wg set $(enquote "${dev}") private-key $(enquote "${skdir}/wgsk")"
|
||||||
|
|
||||||
|
|
||||||
|
if test -n "${lport}"; then
|
||||||
|
frag_append "listen-port $(enquote "$(( lport + 1 ))")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
frag "
|
||||||
|
# Launch the post quantum wireguard exchange daemon
|
||||||
|
$(enquote "${binary}") exchange"
|
||||||
|
|
||||||
|
if (( verbose == 1 )); then
|
||||||
|
frag_append "verbose"
|
||||||
|
fi
|
||||||
|
|
||||||
|
frag_append_esc " private-key $(enquote "${skdir}/pqsk")"
|
||||||
|
frag_append_esc " public-key $(enquote "${skdir}/pqpk")"
|
||||||
|
|
||||||
|
if test -n "${lport}"; then
|
||||||
|
frag_append_esc " listen $(enquote "${lip}:${lport}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
usagestack+=("peer" "PUBLIC_KEYS_DIR endpoint IP:PORT")
|
||||||
|
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
shift; # Skip "peer" argument
|
||||||
|
|
||||||
|
local peerdir ip port keepalive allowedips
|
||||||
|
peerdir="${1/\//}"; shift || fatal "Required peer argument: PUBLIC_KEYS_DIR"
|
||||||
|
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
local arg; arg="$1"; shift
|
||||||
|
case "${arg}" in
|
||||||
|
peer) set -- "peer" "$@"; break;; # Next peer
|
||||||
|
endpoint) ip="${1%:*}"; port="${1/*:/}"; shift;;
|
||||||
|
persistent-keepalive) keepalive="${1}"; shift;;
|
||||||
|
allowed-ips) allowedips="${1}"; shift;;
|
||||||
|
-h | -help | --help | help) usage; return 0;;
|
||||||
|
*) fatal "Unknown option ${arg}";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Public key
|
||||||
|
frag_append_esc " peer public-key $(enquote "${peerdir}/pqpk")"
|
||||||
|
|
||||||
|
# PSK
|
||||||
|
local pskfile; pskfile="${peerdir}/psk"
|
||||||
|
if test -f "${pskfile}"; then
|
||||||
|
frag_append_esc " preshared-key $(enquote "${pskfile}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
if test -n "${ip}"; then
|
||||||
|
frag_append_esc " endpoint $(enquote "${ip}:${port}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
frag_append_esc " wireguard $(enquote "${dev}") $(enquote "$(cat "${peerdir}/wgpk")")"
|
||||||
|
|
||||||
|
if test -n "${ip}"; then
|
||||||
|
frag_append_esc " endpoint $(enquote "${ip}:$(( port + 1 ))")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -n "${keepalive}"; then
|
||||||
|
frag_append_esc " persistent-keepalive $(enquote "${keepalive}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -n "${allowedips}"; then
|
||||||
|
frag_append_esc " allowed-ips $(enquote "${allowedips}")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
formatting_init
|
||||||
|
cleanup_init
|
||||||
|
usage_init
|
||||||
|
frag_init
|
||||||
|
|
||||||
|
project_name="rosenpass"
|
||||||
|
scriptdir="$(dirname "${script}")"
|
||||||
|
verbose=0
|
||||||
|
binary="$(
|
||||||
|
find "${scriptdir}"/target/{release,debug}/"${project_name}" -printf "%T@ %p\n" 2>/dev/null \
|
||||||
|
| sort -nr \
|
||||||
|
| awk -v fallback="${project_name}" '
|
||||||
|
NR == 1 { print($2) }
|
||||||
|
END { if (NR == 0) print(fallback) }'
|
||||||
|
)"
|
||||||
|
|
||||||
|
# Parse command
|
||||||
|
|
||||||
|
usagestack+=("[explain]" "[verbose]" "genkey|pubkey|exchange" "[ARGS]...")
|
||||||
|
|
||||||
|
local cmd
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
local arg; arg="$1"; shift
|
||||||
|
case "${arg}" in
|
||||||
|
genkey|pubkey|exchange) cmd="${arg}"; break;;
|
||||||
|
explain) explain=1;;
|
||||||
|
verbose) verbose=1;;
|
||||||
|
-h | -help | --help | help) usage; return 0 ;;
|
||||||
|
*) fatal "Unknown command ${arg}";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
test -n "${cmd}" || fatal "No command supplied"
|
||||||
|
usagestack=("${script}")
|
||||||
|
|
||||||
|
# Execute command
|
||||||
|
|
||||||
|
usagestack+=("${cmd}")
|
||||||
|
"${cmd}" "$@"
|
||||||
|
usagestack=("${script}")
|
||||||
|
|
||||||
|
# Apply transaction
|
||||||
|
|
||||||
|
frag_apply
|
||||||
|
}
|
||||||
|
|
||||||
|
script="$0"
|
||||||
|
main "$@"
|
||||||
358
src/coloring.rs
Normal file
358
src/coloring.rs
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
//! This module contains various types for dealing with secrets
|
||||||
|
//!
|
||||||
|
//! These types use type level coloring to make accidential leackage of secrets extra hard.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
sodium::{rng, zeroize},
|
||||||
|
util::{cpy, mutating},
|
||||||
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use libsodium_sys as libsodium;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
convert::TryInto,
|
||||||
|
fmt,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
os::raw::c_void,
|
||||||
|
ptr::null_mut,
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This might become a problem in library usage; it's effectively a memory
|
||||||
|
// leak which probably isn't a problem right now because most memory will
|
||||||
|
// be reused…
|
||||||
|
lazy_static! {
|
||||||
|
static ref SECRET_CACHE: Mutex<SecretMemoryPool> = Mutex::new(SecretMemoryPool::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pool that stores secret memory allocations
|
||||||
|
///
|
||||||
|
/// Allocation of secret memory is expensive. Thus, this struct provides a
|
||||||
|
/// pool of secret memory, readily available to yield protected, slices of
|
||||||
|
/// memory.
|
||||||
|
///
|
||||||
|
/// Further information about the protection in place can be found in in the
|
||||||
|
/// [libsodium documentation](https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations)
|
||||||
|
#[derive(Debug)] // TODO check on Debug derive, is that clever
|
||||||
|
pub struct SecretMemoryPool {
|
||||||
|
pool: HashMap<usize, Vec<*mut c_void>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecretMemoryPool {
|
||||||
|
/// Create a new [SecretMemoryPool]
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let pool = HashMap::new();
|
||||||
|
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return secrete back to the pool for future re-use
|
||||||
|
///
|
||||||
|
/// This consumes the [Secret], but its memory is re-used.
|
||||||
|
pub fn release<const N: usize>(&mut self, mut s: Secret<N>) {
|
||||||
|
unsafe {
|
||||||
|
self.release_by_ref(&mut s);
|
||||||
|
}
|
||||||
|
std::mem::forget(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return secret back to the pool for future re-use, by slice
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// After calling this function on a [Secret], the secret must never be
|
||||||
|
/// used again for anything.
|
||||||
|
unsafe fn release_by_ref<const N: usize>(&mut self, s: &mut Secret<N>) {
|
||||||
|
s.zeroize();
|
||||||
|
let Secret { ptr: secret } = s;
|
||||||
|
// don't call Secret::drop, that could cause a double free
|
||||||
|
self.pool.entry(N).or_default().push(*secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take protected memory from the pool, allocating new one if no suitable
|
||||||
|
/// chunk is found in the inventory.
|
||||||
|
///
|
||||||
|
/// The secret is guaranteed to be full of nullbytes
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This function contains an unsafe call to [libsodium::sodium_malloc].
|
||||||
|
/// This call has no known safety invariants, thus nothing can go wrong™.
|
||||||
|
/// However, just like normal `malloc()` this can return a null ptr. Thus
|
||||||
|
/// the returned pointer is checked for null; causing the program to panic
|
||||||
|
/// if it is null.
|
||||||
|
pub fn take<const N: usize>(&mut self) -> Secret<N> {
|
||||||
|
let entry = self.pool.entry(N).or_default();
|
||||||
|
let secret = entry.pop().unwrap_or_else(|| {
|
||||||
|
let ptr = unsafe { libsodium::sodium_malloc(N) };
|
||||||
|
assert!(
|
||||||
|
!ptr.is_null(),
|
||||||
|
"libsodium::sodium_mallloc() returned a null ptr"
|
||||||
|
);
|
||||||
|
ptr
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut s = Secret { ptr: secret };
|
||||||
|
s.zeroize();
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SecretMemoryPool {
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The drop implementation frees the contained elements using
|
||||||
|
/// [libsodium::sodium_free]. This is safe as long as every `*mut c_void`
|
||||||
|
/// contained was initialized with a call to [libsodium::sodium_malloc]
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for ptr in self.pool.drain().flat_map(|(_, x)| x.into_iter()) {
|
||||||
|
unsafe {
|
||||||
|
libsodium::sodium_free(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// No safety implications are known, since the `*mut c_void` in
|
||||||
|
/// is essentially used like a `&mut u8` [SecretMemoryPool].
|
||||||
|
unsafe impl Send for SecretMemoryPool {}
|
||||||
|
|
||||||
|
/// Store for a secret
|
||||||
|
///
|
||||||
|
/// Uses memory allocated with [libsodium::sodium_malloc],
|
||||||
|
/// esentially can do the same things as `[u8; N].as_mut_ptr()`.
|
||||||
|
pub struct Secret<const N: usize> {
|
||||||
|
ptr: *mut c_void,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Clone for Secret<N> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
let mut new = Self::zero();
|
||||||
|
new.secret_mut().clone_from_slice(self.secret());
|
||||||
|
new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Drop for Secret<N> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.zeroize();
|
||||||
|
// the invariant that the [Secret] is not used after the
|
||||||
|
// `release_by_ref` call is guaranteed, since this is a drop implementation
|
||||||
|
unsafe { SECRET_CACHE.lock().unwrap().release_by_ref(self) };
|
||||||
|
self.ptr = null_mut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Secret<N> {
|
||||||
|
pub fn from_slice(slice: &[u8]) -> Self {
|
||||||
|
let mut new_self = Self::zero();
|
||||||
|
new_self.secret_mut().copy_from_slice(slice);
|
||||||
|
new_self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [Secret] that is zero initialized
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
// Using [SecretMemoryPool] here because this operation is expensive,
|
||||||
|
// yet it is used in hot loops
|
||||||
|
let s = SECRET_CACHE.lock().unwrap().take();
|
||||||
|
assert_eq!(s.secret(), &[0u8; N]);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [Secret] that is randomized
|
||||||
|
pub fn random() -> Self {
|
||||||
|
mutating(Self::zero(), |r| r.randomize())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets all data of an existing secret to null bytes
|
||||||
|
pub fn zeroize(&mut self) {
|
||||||
|
zeroize(self.secret_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets all data an existing secret to random bytes
|
||||||
|
pub fn randomize(&mut self) {
|
||||||
|
rng(self.secret_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the data
|
||||||
|
pub fn secret(&self) -> &[u8; N] {
|
||||||
|
// - calling `from_raw_parts` is safe, because `ptr` is initalized with
|
||||||
|
// as `N` byte allocation from the creation of `Secret` onwards. `ptr`
|
||||||
|
// stays valid over the full lifetime of `Secret`
|
||||||
|
//
|
||||||
|
// - calling uwnrap is safe, because we can guarantee that the slice has
|
||||||
|
// exactly the required size `N` to create an array of `N` elements.
|
||||||
|
let ptr = self.ptr as *const u8;
|
||||||
|
let slice = unsafe { std::slice::from_raw_parts(ptr, N) };
|
||||||
|
slice.try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the data mutably
|
||||||
|
pub fn secret_mut(&mut self) -> &mut [u8; N] {
|
||||||
|
// the same safety argument as for `secret()` holds
|
||||||
|
let ptr = self.ptr as *mut u8;
|
||||||
|
let slice = unsafe { std::slice::from_raw_parts_mut(ptr, N) };
|
||||||
|
slice.try_into().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Debug implementation of [Secret] does not reveal the secret data,
|
||||||
|
/// instead a placeholder `<SECRET>` is used
|
||||||
|
impl<const N: usize> fmt::Debug for Secret<N> {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
fmt.write_str("<SECRET>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains information in the form of a byte array that may be known to the
|
||||||
|
/// public
|
||||||
|
// TODO: We should get rid of the Public type; just use a normal value
|
||||||
|
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Public<const N: usize> {
|
||||||
|
pub value: [u8; N],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Public<N> {
|
||||||
|
/// Create a new [Public] from a byte slice
|
||||||
|
pub fn from_slice(value: &[u8]) -> Self {
|
||||||
|
mutating(Self::zero(), |r| cpy(value, &mut r.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [Public] from a byte array
|
||||||
|
pub fn new(value: [u8; N]) -> Self {
|
||||||
|
Self { value }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a zero initialized [Public]
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self { value: [0u8; N] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a random initialized [Public]
|
||||||
|
pub fn random() -> Self {
|
||||||
|
mutating(Self::zero(), |r| r.randomize())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Randomize all bytes in an existing [Public]
|
||||||
|
pub fn randomize(&mut self) {
|
||||||
|
rng(&mut self.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the contents of an `&[u8]` as hexadecimal symbols to a [std::fmt::Formatter]
|
||||||
|
pub fn debug_crypto_array(v: &[u8], fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
fmt.write_str("[{}]=")?;
|
||||||
|
if v.len() > 64 {
|
||||||
|
for byte in &v[..32] {
|
||||||
|
std::fmt::LowerHex::fmt(byte, fmt)?;
|
||||||
|
}
|
||||||
|
fmt.write_str("…")?;
|
||||||
|
for byte in &v[v.len() - 32..] {
|
||||||
|
std::fmt::LowerHex::fmt(byte, fmt)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for byte in v {
|
||||||
|
std::fmt::LowerHex::fmt(byte, fmt)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> fmt::Debug for Public<N> {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
debug_crypto_array(&self.value, fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Deref for Public<N> {
|
||||||
|
type Target = [u8; N];
|
||||||
|
|
||||||
|
fn deref(&self) -> &[u8; N] {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> DerefMut for Public<N> {
|
||||||
|
fn deref_mut(&mut self) -> &mut [u8; N] {
|
||||||
|
&mut self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations
|
||||||
|
/// promises us that allocated memory is initialized with this magic byte
|
||||||
|
const SODIUM_MAGIC_BYTE: u8 = 0xdb;
|
||||||
|
|
||||||
|
/// must be called before any interaction with libsodium
|
||||||
|
fn init() {
|
||||||
|
unsafe { libsodium_sys::sodium_init() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// checks that whe can malloc with libsodium
|
||||||
|
#[test]
|
||||||
|
fn sodium_malloc() {
|
||||||
|
init();
|
||||||
|
const N: usize = 8;
|
||||||
|
let ptr = unsafe { libsodium_sys::sodium_malloc(N) };
|
||||||
|
let mem = unsafe { std::slice::from_raw_parts(ptr as *mut u8, N) };
|
||||||
|
assert_eq!(mem, &[SODIUM_MAGIC_BYTE; N])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// checks that whe can free with libsodium
|
||||||
|
#[test]
|
||||||
|
fn sodium_free() {
|
||||||
|
init();
|
||||||
|
const N: usize = 8;
|
||||||
|
let ptr = unsafe { libsodium_sys::sodium_malloc(N) };
|
||||||
|
unsafe { libsodium_sys::sodium_free(ptr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// check that we can alloc using the magic pool
|
||||||
|
#[test]
|
||||||
|
fn secret_memory_pool_take() {
|
||||||
|
init();
|
||||||
|
const N: usize = 0x100;
|
||||||
|
let mut pool = SecretMemoryPool::new();
|
||||||
|
let secret: Secret<N> = pool.take();
|
||||||
|
assert_eq!(secret.secret(), &[0; N]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// check that a secrete lives, even if its [SecretMemoryPool] is deleted
|
||||||
|
#[test]
|
||||||
|
fn secret_memory_pool_drop() {
|
||||||
|
init();
|
||||||
|
const N: usize = 0x100;
|
||||||
|
let mut pool = SecretMemoryPool::new();
|
||||||
|
let secret: Secret<N> = pool.take();
|
||||||
|
std::mem::drop(pool);
|
||||||
|
assert_eq!(secret.secret(), &[0; N]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// check that a secrete can be reborn, freshly initialized with zero
|
||||||
|
#[test]
|
||||||
|
fn secret_memory_pool_release() {
|
||||||
|
init();
|
||||||
|
const N: usize = 1;
|
||||||
|
let mut pool = SecretMemoryPool::new();
|
||||||
|
let mut secret: Secret<N> = pool.take();
|
||||||
|
let old_secret_ptr = secret.ptr;
|
||||||
|
|
||||||
|
secret.secret_mut()[0] = 0x13;
|
||||||
|
pool.release(secret);
|
||||||
|
|
||||||
|
// now check that we get the same ptr
|
||||||
|
let new_secret: Secret<N> = pool.take();
|
||||||
|
assert_eq!(old_secret_ptr, new_secret.ptr);
|
||||||
|
|
||||||
|
// and that the secret was zeroized
|
||||||
|
assert_eq!(new_secret.secret(), &[0; N]);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/labeled_prf.rs
Normal file
45
src/labeled_prf.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use {
|
||||||
|
crate::{prftree::PrfTree, sodium::KEY_SIZE},
|
||||||
|
anyhow::Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn protocol() -> Result<PrfTree> {
|
||||||
|
PrfTree::zero().mix("Rosenpass v1 mceliece460896 Kyber512 ChaChaPoly1305 BLAKE2s".as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Use labels that can serve as idents
|
||||||
|
macro_rules! prflabel {
|
||||||
|
($base:ident, $name:ident, $($lbl:expr),* ) => {
|
||||||
|
pub fn $name() -> Result<PrfTree> {
|
||||||
|
let t = $base()?;
|
||||||
|
$( let t = t.mix($lbl.as_bytes())?; )*
|
||||||
|
Ok(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prflabel!(protocol, mac, "mac");
|
||||||
|
prflabel!(protocol, cookie, "cookie");
|
||||||
|
prflabel!(protocol, peerid, "peer id");
|
||||||
|
prflabel!(protocol, biscuit_ad, "biscuit additional data");
|
||||||
|
prflabel!(protocol, ckinit, "chaining key init");
|
||||||
|
prflabel!(protocol, _ckextract, "chaining key extract");
|
||||||
|
|
||||||
|
macro_rules! prflabel_leaf {
|
||||||
|
($base:ident, $name:ident, $($lbl:expr),* ) => {
|
||||||
|
pub fn $name() -> Result<[u8; KEY_SIZE]> {
|
||||||
|
let t = $base()?;
|
||||||
|
$( let t = t.mix($lbl.as_bytes())?; )*
|
||||||
|
Ok(t.into_value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prflabel_leaf!(_ckextract, mix, "mix");
|
||||||
|
prflabel_leaf!(_ckextract, hs_enc, "handshake encryption");
|
||||||
|
prflabel_leaf!(_ckextract, ini_enc, "initiator handshake encryption");
|
||||||
|
prflabel_leaf!(_ckextract, res_enc, "responder handshake encryption");
|
||||||
|
|
||||||
|
prflabel!(_ckextract, _user, "user");
|
||||||
|
prflabel!(_user, _rp, "rosenpass.eu");
|
||||||
|
prflabel_leaf!(_rp, osk, "wireguard psk");
|
||||||
56
src/lib.rs
Normal file
56
src/lib.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#[macro_use]
|
||||||
|
pub mod util;
|
||||||
|
#[macro_use]
|
||||||
|
pub mod sodium;
|
||||||
|
pub mod coloring;
|
||||||
|
pub mod labeled_prf;
|
||||||
|
pub mod msgs;
|
||||||
|
pub mod pqkem;
|
||||||
|
pub mod prftree;
|
||||||
|
pub mod protocol;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum RosenpassError {
|
||||||
|
#[error("error in OQS")]
|
||||||
|
Oqs,
|
||||||
|
#[error("error from external library while calling OQS")]
|
||||||
|
OqsExternalLib,
|
||||||
|
#[error("buffer size mismatch, required {required_size} but only found {actual_size}")]
|
||||||
|
BufferSizeMismatch {
|
||||||
|
required_size: usize,
|
||||||
|
actual_size: usize,
|
||||||
|
},
|
||||||
|
#[error("invalid message type")]
|
||||||
|
InvalidMessageType(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RosenpassError {
|
||||||
|
/// Helper function to check a buffer size
|
||||||
|
fn check_buffer_size(required_size: usize, actual_size: usize) -> Result<(), Self> {
|
||||||
|
if required_size != actual_size {
|
||||||
|
Err(Self::BufferSizeMismatch {
|
||||||
|
required_size,
|
||||||
|
actual_size,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait to attach function calls to foreign types.
|
||||||
|
trait RosenpassMaybeError {
|
||||||
|
/// Checks whether something is an error or not
|
||||||
|
fn to_rg_error(&self) -> Result<(), RosenpassError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RosenpassMaybeError for oqs_sys::common::OQS_STATUS {
|
||||||
|
fn to_rg_error(&self) -> Result<(), RosenpassError> {
|
||||||
|
use oqs_sys::common::OQS_STATUS;
|
||||||
|
match self {
|
||||||
|
OQS_STATUS::OQS_SUCCESS => Ok(()),
|
||||||
|
OQS_STATUS::OQS_ERROR => Err(RosenpassError::Oqs),
|
||||||
|
OQS_STATUS::OQS_EXTERNAL_LIB_ERROR_OPENSSL => Err(RosenpassError::OqsExternalLib),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/lprf.rs
Normal file
106
src/lprf.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
//! The rosenpass protocol relies on a special type
|
||||||
|
//! of hash function for most of its hashing or
|
||||||
|
//! message authentication needs: an incrementable
|
||||||
|
//! pseudo random function.
|
||||||
|
//!
|
||||||
|
//! This is a generalization of a PRF operating
|
||||||
|
//! on a sequence of inputs instead of a single input.
|
||||||
|
//!
|
||||||
|
//! Like a Dec function the Iprf features efficient
|
||||||
|
//! incrementability.
|
||||||
|
//!
|
||||||
|
//! You can also think of an Iprf as a Dec function with
|
||||||
|
//! a fixed size output.
|
||||||
|
//!
|
||||||
|
//! The idea behind a Iprf is that it can be efficiently
|
||||||
|
//! constructed from an Dec function as well as a PRF.
|
||||||
|
//!
|
||||||
|
//! TODO Base the construction on a proper Dec function
|
||||||
|
|
||||||
|
pub struct Iprf([u8; KEY_SIZE]);
|
||||||
|
pub struct IprfBranch([u8; KEY_SIZE]);
|
||||||
|
pub struct SecretIprf(Secret<KEY_SIZE>);
|
||||||
|
pub struct SecretIprfBranch(Secret<KEY_SIZE>);
|
||||||
|
|
||||||
|
pub fn prf_into(out: &mut [u8], key: &[u8], data: &[u8]) {
|
||||||
|
// TODO: The error handling with sodium is a scurge
|
||||||
|
hmac_into(out, key, data).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prf(key: &[u8], data: &[u8]) -> [u8; KEY_SIZE]{
|
||||||
|
mutating([0u8; KEY_SIZE], |r| prf_into(r, key, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iprf {
|
||||||
|
fn zero() -> Self {
|
||||||
|
Self([0u8; KEY_SIZE])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dup(self) -> IprfBranch {
|
||||||
|
IprfBranch(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Protocol! Use domain separation to ensure that
|
||||||
|
fn mix(self, v: &[u8]) -> Self {
|
||||||
|
Self(prf(&self.0, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mix_secret<const N: usize>(self, v: Secret<N>) -> SecretIprf {
|
||||||
|
SecretIprf::prf_invoc(&self.0, v.secret())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_value(self) -> [u8; KEY_SIZE] {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract(self, v: &[u8], dst: &mut [u8]) {
|
||||||
|
prf_into(&self.0, v, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IprfBranch {
|
||||||
|
fn mix(&self, v: &[u8]) -> Iprf {
|
||||||
|
Iprf(prf(self.0, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mix_secret<const N: usize>(&self, v: Secret<N>) -> SecretIprf {
|
||||||
|
SecretIprf::prf_incov(self.0, v.secret())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecretIprf {
|
||||||
|
fn prf_invoc(k: &[u8], d: &[u8]) -> SecretIprf {
|
||||||
|
mutating(SecretIprf(Secret::zero()), |r|
|
||||||
|
prf_into(k, d, r.secret_mut()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_key(k: Secret<N>) -> SecretIprf {
|
||||||
|
Self(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mix(self, v: &[u8]) -> SecretIprf {
|
||||||
|
Self::prf_invoc(self.0.secret(), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mix_secret<const N: usize>(self, v: Secret<N>) -> SecretIprf {
|
||||||
|
Self::prf_invoc(self.0.secret(), v.secret())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_secret(self) -> Secret<KEY_SIZE> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_secret_slice(self, v: &[u8], dst: &[u8]) {
|
||||||
|
prf_into(self.0.secret(), v, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecretIprfBranch {
|
||||||
|
fn mix(&self, v: &[u8]) -> SecretIprf {
|
||||||
|
SecretIprf::prf_invoc(self.0.secret(), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mix_secret<const N: usize>(&self, v: Secret<N>) -> SecretIprf {
|
||||||
|
SecretIprf::prf_invoc(self.0.secret(), v.secret())
|
||||||
|
}
|
||||||
|
}
|
||||||
646
src/main.rs
Normal file
646
src/main.rs
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
use anyhow::{bail, ensure, Context, Result};
|
||||||
|
use log::{error, info};
|
||||||
|
use rosenpass::{
|
||||||
|
attempt,
|
||||||
|
coloring::{Public, Secret},
|
||||||
|
multimatch,
|
||||||
|
pqkem::{SKEM, KEM},
|
||||||
|
protocol::{SPk, SSk, MsgBuf, PeerPtr, Server as CryptoServer, SymKey, Timing},
|
||||||
|
sodium::sodium_init,
|
||||||
|
util::{b64_reader, b64_writer, fmt_b64},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fs::{File, OpenOptions},
|
||||||
|
io::{ErrorKind, Read, Write},
|
||||||
|
net::{SocketAddr, ToSocketAddrs, UdpSocket},
|
||||||
|
path::Path,
|
||||||
|
process::{exit, Command, Stdio},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Open a file writable
|
||||||
|
pub fn fopen_w<P: AsRef<Path>>(path: P) -> Result<File> {
|
||||||
|
Ok(OpenOptions::new()
|
||||||
|
.read(false)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(path)?)
|
||||||
|
}
|
||||||
|
/// Open a file readable
|
||||||
|
pub fn fopen_r<P: AsRef<Path>>(path: P) -> Result<File> {
|
||||||
|
Ok(OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(false)
|
||||||
|
.create(false)
|
||||||
|
.truncate(false)
|
||||||
|
.open(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ReadExactToEnd {
|
||||||
|
fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read> ReadExactToEnd for R {
|
||||||
|
fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()> {
|
||||||
|
let mut dummy = [0u8; 8];
|
||||||
|
self.read_exact(buf)?;
|
||||||
|
ensure!(self.read(&mut dummy)? == 0, "File too long!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait LoadValue {
|
||||||
|
fn load<P: AsRef<Path>>(path: P) -> Result<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait LoadValueB64 {
|
||||||
|
fn load_b64<P: AsRef<Path>>(path: P) -> Result<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
trait StoreValue {
|
||||||
|
fn store<P: AsRef<Path>>(&self, path: P) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
trait StoreSecret {
|
||||||
|
unsafe fn store_secret<P: AsRef<Path>>(&self, path: P) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: StoreValue> StoreSecret for T {
|
||||||
|
unsafe fn store_secret<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||||
|
self.store(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> LoadValue for Secret<N> {
|
||||||
|
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
let mut v = Self::random();
|
||||||
|
let p = path.as_ref();
|
||||||
|
fopen_r(p)?
|
||||||
|
.read_exact_to_end(v.secret_mut())
|
||||||
|
.with_context(|| format!("Could not load file {p:?}"))?;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> LoadValueB64 for Secret<N> {
|
||||||
|
fn load_b64<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
let mut v = Self::random();
|
||||||
|
let p = path.as_ref();
|
||||||
|
// This might leave some fragments of the secret on the stack;
|
||||||
|
// in practice this is likely not a problem because the stack likely
|
||||||
|
// will be overwritten by something else soon but this is not exactly
|
||||||
|
// guaranteed. It would be possible to remedy this, but since the secret
|
||||||
|
// data will linger in the linux page cache anyways with the current
|
||||||
|
// implementation, going to great length to erase the secret here is
|
||||||
|
// not worth it right now.
|
||||||
|
b64_reader(&mut fopen_r(p)?)
|
||||||
|
.read_exact(v.secret_mut())
|
||||||
|
.with_context(|| format!("Could not load base64 file {p:?}"))?;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> StoreSecret for Secret<N> {
|
||||||
|
unsafe fn store_secret<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||||
|
std::fs::write(path, self.secret())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> LoadValue for Public<N> {
|
||||||
|
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
let mut v = Self::random();
|
||||||
|
fopen_r(path)?.read_exact_to_end(&mut *v)?;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> StoreValue for Public<N> {
|
||||||
|
fn store<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||||
|
std::fs::write(path, **self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! bail_usage {
|
||||||
|
($args:expr, $($pt:expr),*) => {{
|
||||||
|
error!($($pt),*);
|
||||||
|
cmd_help()?;
|
||||||
|
exit(1);
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! ensure_usage {
|
||||||
|
($args:expr, $ck:expr, $($pt:expr),*) => {{
|
||||||
|
if !$ck {
|
||||||
|
bail_usage!($args, $($pt),*);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! mandatory_opt {
|
||||||
|
($args:expr, $val:expr, $name:expr) => {{
|
||||||
|
ensure_usage!($args, $val.is_some(), "{0} option is mandatory", $name)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArgsWalker {
|
||||||
|
pub argv: Vec<String>,
|
||||||
|
pub off: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArgsWalker {
|
||||||
|
pub fn get(&self) -> Option<&str> {
|
||||||
|
self.argv.get(self.off).map(|s| s as &str)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&mut self) -> Option<&str> {
|
||||||
|
assert!(self.off > 0);
|
||||||
|
self.off -= 1;
|
||||||
|
self.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn next(&mut self) -> Option<&str> {
|
||||||
|
assert!(self.todo() > 0);
|
||||||
|
self.off += 1;
|
||||||
|
self.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn opt(&mut self, dst: &mut Option<String>) -> Result<()> {
|
||||||
|
let cmd = &self.argv[self.off - 1];
|
||||||
|
ensure_usage!(&self, self.todo() > 0, "Option {} takes a value", cmd);
|
||||||
|
ensure_usage!(&self, dst.is_none(), "Cannot set {} multiple times.", cmd);
|
||||||
|
*dst = Some(String::from(self.next().unwrap()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn todo(&self) -> usize {
|
||||||
|
self.argv.len() - self.off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct WireguardOut {
|
||||||
|
// impl KeyOutput
|
||||||
|
dev: String,
|
||||||
|
pk: String,
|
||||||
|
extra_params: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct AppPeer {
|
||||||
|
pub outfile: Option<String>,
|
||||||
|
pub outwg: Option<WireguardOut>,
|
||||||
|
pub tx_addr: Option<SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Verbosity {
|
||||||
|
Quiet,
|
||||||
|
Verbose,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds the state of the application, namely the external IO
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AppServer {
|
||||||
|
pub crypt: CryptoServer,
|
||||||
|
pub sock: UdpSocket,
|
||||||
|
pub peers: Vec<AppPeer>,
|
||||||
|
pub verbosity: Verbosity,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Index based pointer to a Peer
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AppPeerPtr(pub usize);
|
||||||
|
|
||||||
|
impl AppPeerPtr {
|
||||||
|
/// Takes an index based handle and returns the actual peer
|
||||||
|
pub fn lift(p: PeerPtr) -> Self {
|
||||||
|
Self(p.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an index based handle to one Peer
|
||||||
|
pub fn lower(&self) -> PeerPtr {
|
||||||
|
PeerPtr(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_app<'a>(&self, srv: &'a AppServer) -> &'a AppPeer {
|
||||||
|
&srv.peers[self.0]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_app_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut AppPeer {
|
||||||
|
&mut srv.peers[self.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppPollResult {
|
||||||
|
DeleteKey(AppPeerPtr),
|
||||||
|
SendInitiation(AppPeerPtr),
|
||||||
|
SendRetransmission(AppPeerPtr),
|
||||||
|
ReceivedMessage(usize, SocketAddr),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum KeyOutputReason {
|
||||||
|
Exchanged,
|
||||||
|
Stale,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Catches errors, prints them through the logger, then exits
|
||||||
|
pub fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
match rosenpass_main() {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{e}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entry point to the whole program
|
||||||
|
pub fn rosenpass_main() -> Result<()> {
|
||||||
|
sodium_init()?;
|
||||||
|
|
||||||
|
let mut args = ArgsWalker {
|
||||||
|
argv: std::env::args().collect(),
|
||||||
|
off: 0, // skipping executable path
|
||||||
|
};
|
||||||
|
|
||||||
|
// Command parsing
|
||||||
|
match args.next() {
|
||||||
|
Some("help") | Some("-h") | Some("-help") | Some("--help") => cmd_help()?,
|
||||||
|
Some("keygen") => cmd_keygen(args)?,
|
||||||
|
Some("exchange") => cmd_exchange(args)?,
|
||||||
|
Some(cmd) => bail_usage!(&args, "No such command {}", cmd),
|
||||||
|
None => bail_usage!(&args, "Expected a command!"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print the usage information
|
||||||
|
pub fn cmd_help() -> Result<()> {
|
||||||
|
eprint!(include_str!("usage.md"), env!("CARGO_BIN_NAME"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a keypair
|
||||||
|
pub fn cmd_keygen(mut args: ArgsWalker) -> Result<()> {
|
||||||
|
let mut sf: Option<String> = None;
|
||||||
|
let mut pf: Option<String> = None;
|
||||||
|
|
||||||
|
// Arg parsing
|
||||||
|
loop {
|
||||||
|
match args.next() {
|
||||||
|
Some("private-key") => args.opt(&mut sf)?,
|
||||||
|
Some("public-key") => args.opt(&mut pf)?,
|
||||||
|
Some(opt) => bail_usage!(&args, "Unknown option `{}`", opt),
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mandatory_opt!(&args, sf, "private-key");
|
||||||
|
mandatory_opt!(&args, pf, "private-key");
|
||||||
|
|
||||||
|
// Cmd
|
||||||
|
let (mut ssk, mut spk) = (SSk::random(), SPk::random());
|
||||||
|
unsafe {
|
||||||
|
SKEM::keygen(ssk.secret_mut(), spk.secret_mut())?;
|
||||||
|
ssk.store_secret(sf.unwrap())?;
|
||||||
|
spk.store_secret(pf.unwrap())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmd_exchange(mut args: ArgsWalker) -> Result<()> {
|
||||||
|
// Argument parsing
|
||||||
|
let mut sf: Option<String> = None;
|
||||||
|
let mut pf: Option<String> = None;
|
||||||
|
let mut listen: Option<String> = None;
|
||||||
|
let mut verbosity = Verbosity::Quiet;
|
||||||
|
|
||||||
|
// Global parameters
|
||||||
|
loop {
|
||||||
|
match args.next() {
|
||||||
|
Some("private-key") => args.opt(&mut sf)?,
|
||||||
|
Some("public-key") => args.opt(&mut pf)?,
|
||||||
|
Some("listen") => args.opt(&mut listen)?,
|
||||||
|
Some("verbose") => {
|
||||||
|
verbosity = Verbosity::Verbose;
|
||||||
|
}
|
||||||
|
Some("peer") => {
|
||||||
|
args.prev();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(opt) => bail_usage!(&args, "Unknown option `{}`", opt),
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mandatory_opt!(&args, sf, "private-key");
|
||||||
|
mandatory_opt!(&args, pf, "public-key");
|
||||||
|
|
||||||
|
let mut srv = std::boxed::Box::<AppServer>::new(AppServer::new(
|
||||||
|
// sk, pk, addr
|
||||||
|
SSk::load(&sf.unwrap())?,
|
||||||
|
SPk::load(&pf.unwrap())?,
|
||||||
|
listen.as_deref().unwrap_or("[0::0]:0"),
|
||||||
|
verbosity,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
// Peer parameters
|
||||||
|
'_parseAllPeers: while args.todo() > 0 {
|
||||||
|
let mut pf: Option<String> = None;
|
||||||
|
let mut outfile: Option<String> = None;
|
||||||
|
let mut outwg: Option<WireguardOut> = None;
|
||||||
|
let mut endpoint: Option<String> = None;
|
||||||
|
let mut pskf: Option<String> = None;
|
||||||
|
|
||||||
|
args.next(); // skip "peer" starter itself
|
||||||
|
|
||||||
|
'parseOnePeer: loop {
|
||||||
|
match args.next() {
|
||||||
|
// Done with this peer
|
||||||
|
Some("peer") => {
|
||||||
|
args.prev();
|
||||||
|
break 'parseOnePeer;
|
||||||
|
}
|
||||||
|
None => break 'parseOnePeer,
|
||||||
|
// Options
|
||||||
|
Some("public-key") => args.opt(&mut pf)?,
|
||||||
|
Some("endpoint") => args.opt(&mut endpoint)?,
|
||||||
|
Some("preshared-key") => args.opt(&mut pskf)?,
|
||||||
|
Some("outfile") => args.opt(&mut outfile)?,
|
||||||
|
// Wireguard out
|
||||||
|
Some("wireguard") => {
|
||||||
|
ensure_usage!(
|
||||||
|
&args,
|
||||||
|
outwg.is_none(),
|
||||||
|
"Cannot set wireguard output for the same peer multiple times."
|
||||||
|
);
|
||||||
|
ensure_usage!(&args, args.todo() >= 2, "Option wireguard takes to values");
|
||||||
|
let dev = String::from(args.next().unwrap());
|
||||||
|
let pk = String::from(args.next().unwrap());
|
||||||
|
let wg = outwg.insert(WireguardOut {
|
||||||
|
dev,
|
||||||
|
pk,
|
||||||
|
extra_params: Vec::new(),
|
||||||
|
});
|
||||||
|
'_parseWgOutExtra: loop {
|
||||||
|
match args.next() {
|
||||||
|
Some("peer") => {
|
||||||
|
args.prev();
|
||||||
|
break 'parseOnePeer;
|
||||||
|
}
|
||||||
|
None => break 'parseOnePeer,
|
||||||
|
Some(xtra) => wg.extra_params.push(xtra.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Invalid
|
||||||
|
Some(opt) => bail_usage!(&args, "Unknown peer option `{}`", opt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mandatory_opt!(&args, pf, "private-key");
|
||||||
|
ensure_usage!(
|
||||||
|
&args,
|
||||||
|
outfile.is_some() || outwg.is_some(),
|
||||||
|
"Either of the outfile or wireguard option is mandatory"
|
||||||
|
);
|
||||||
|
|
||||||
|
let tx_addr = endpoint
|
||||||
|
.map(|e| {
|
||||||
|
e.to_socket_addrs()?
|
||||||
|
.next()
|
||||||
|
.context("Expected address in endpoint parameter")
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
srv.add_peer(
|
||||||
|
// psk, pk, outfile, outwg, tx_addr
|
||||||
|
pskf.map(SymKey::load_b64).transpose()?,
|
||||||
|
SPk::load(&pf.unwrap())?,
|
||||||
|
outfile,
|
||||||
|
outwg,
|
||||||
|
tx_addr,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.listen_loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppServer {
|
||||||
|
pub fn new<A: ToSocketAddrs>(
|
||||||
|
sk: SSk,
|
||||||
|
pk: SPk,
|
||||||
|
addr: A,
|
||||||
|
verbosity: Verbosity,
|
||||||
|
) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
crypt: CryptoServer::new(sk, pk),
|
||||||
|
sock: UdpSocket::bind(addr)?,
|
||||||
|
peers: Vec::new(),
|
||||||
|
verbosity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verbose(&self) -> bool {
|
||||||
|
matches!(self.verbosity, Verbosity::Verbose)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_peer(
|
||||||
|
&mut self,
|
||||||
|
psk: Option<SymKey>,
|
||||||
|
pk: SPk,
|
||||||
|
outfile: Option<String>,
|
||||||
|
outwg: Option<WireguardOut>,
|
||||||
|
tx_addr: Option<SocketAddr>,
|
||||||
|
) -> Result<AppPeerPtr> {
|
||||||
|
let PeerPtr(pn) = self.crypt.add_peer(psk, pk)?;
|
||||||
|
assert!(pn == self.peers.len());
|
||||||
|
self.peers.push(AppPeer {
|
||||||
|
outfile,
|
||||||
|
outwg,
|
||||||
|
tx_addr,
|
||||||
|
});
|
||||||
|
Ok(AppPeerPtr(pn))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen_loop(&mut self) -> Result<()> {
|
||||||
|
const INIT_SLEEP: f64 = 0.01;
|
||||||
|
const MAX_FAILURES: i32 = 10;
|
||||||
|
let mut failure_cnt = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let msgs_processed = 0usize;
|
||||||
|
let err = match self.event_loop() {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(e) => e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This should not happen…
|
||||||
|
failure_cnt = if msgs_processed > 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
failure_cnt + 1
|
||||||
|
};
|
||||||
|
let sleep = INIT_SLEEP * 2.0f64.powf(f64::from(failure_cnt - 1));
|
||||||
|
let tries_left = MAX_FAILURES - (failure_cnt - 1);
|
||||||
|
error!(
|
||||||
|
"unexpected error after processing {} messages: {:?} {}",
|
||||||
|
msgs_processed,
|
||||||
|
err,
|
||||||
|
err.backtrace()
|
||||||
|
);
|
||||||
|
if tries_left > 0 {
|
||||||
|
error!("reinitializing networking in {sleep}! {tries_left} tries left.");
|
||||||
|
std::thread::sleep(self.crypt.timebase.dur(sleep));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("too many network failures");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn event_loop(&mut self) -> Result<()> {
|
||||||
|
let (mut rx, mut tx) = (MsgBuf::zero(), MsgBuf::zero());
|
||||||
|
macro_rules! tx_maybe_with {
|
||||||
|
($peer:expr, $fn:expr) => {
|
||||||
|
attempt!({
|
||||||
|
let p = $peer.get_app(self);
|
||||||
|
if let Some(addr) = p.tx_addr {
|
||||||
|
let len = $fn()?;
|
||||||
|
self.sock.send_to(&tx[..len], addr)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
use rosenpass::protocol::HandleMsgResult;
|
||||||
|
use AppPollResult::*;
|
||||||
|
use KeyOutputReason::*;
|
||||||
|
match self.poll(&mut *rx)? {
|
||||||
|
SendInitiation(peer) => tx_maybe_with!(peer, || self
|
||||||
|
.crypt
|
||||||
|
.initiate_handshake(peer.lower(), &mut *tx))?,
|
||||||
|
SendRetransmission(peer) => tx_maybe_with!(peer, || self
|
||||||
|
.crypt
|
||||||
|
.retransmit_handshake(peer.lower(), &mut *tx))?,
|
||||||
|
DeleteKey(peer) => self.output_key(peer, Stale, &SymKey::random())?,
|
||||||
|
|
||||||
|
ReceivedMessage(len, addr) => {
|
||||||
|
multimatch!(self.crypt.handle_msg(&rx[..len], &mut *tx),
|
||||||
|
Err(ref e) =>
|
||||||
|
self.verbose().then(||
|
||||||
|
info!("error processing incoming message from {:?}: {:?} {}", addr, e, e.backtrace())),
|
||||||
|
|
||||||
|
Ok(HandleMsgResult { resp: Some(len), .. }) => {
|
||||||
|
self.sock.send_to(&tx[0..len], addr)?
|
||||||
|
},
|
||||||
|
|
||||||
|
Ok(HandleMsgResult { exchanged_with: Some(p), .. }) => {
|
||||||
|
let ap = AppPeerPtr::lift(p);
|
||||||
|
ap.get_app_mut(self).tx_addr = Some(addr);
|
||||||
|
// TODO: Maybe we should rather call the key "rosenpass output"?
|
||||||
|
self.output_key(ap, Exchanged, &self.crypt.osk(p)?)?;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_key(&self, peer: AppPeerPtr, why: KeyOutputReason, key: &SymKey) -> Result<()> {
|
||||||
|
let peerid = peer.lower().get(&self.crypt).pidt()?;
|
||||||
|
let ap = peer.get_app(self);
|
||||||
|
|
||||||
|
if self.verbose() {
|
||||||
|
let msg = match why {
|
||||||
|
KeyOutputReason::Exchanged => "Exchanged key with peer",
|
||||||
|
KeyOutputReason::Stale => "Erasing outdated key from peer",
|
||||||
|
};
|
||||||
|
info!("{} {}", msg, fmt_b64(&*peerid));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(of) = ap.outfile.as_ref() {
|
||||||
|
// This might leave some fragments of the secret on the stack;
|
||||||
|
// in practice this is likely not a problem because the stack likely
|
||||||
|
// will be overwritten by something else soon but this is not exactly
|
||||||
|
// guaranteed. It would be possible to remedy this, but since the secret
|
||||||
|
// data will linger in the linux page cache anyways with the current
|
||||||
|
// implementation, going to great length to erase the secret here is
|
||||||
|
// not worth it right now.
|
||||||
|
b64_writer(fopen_w(of)?).write_all(key.secret())?;
|
||||||
|
let why = match why {
|
||||||
|
KeyOutputReason::Exchanged => "exchanged",
|
||||||
|
KeyOutputReason::Stale => "stale",
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"output-key peer {} key-file {} {}",
|
||||||
|
fmt_b64(&*peerid),
|
||||||
|
of,
|
||||||
|
why
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(owg) = ap.outwg.as_ref() {
|
||||||
|
let child = Command::new("wg")
|
||||||
|
.arg("set")
|
||||||
|
.arg(&owg.dev)
|
||||||
|
.arg("peer")
|
||||||
|
.arg(&owg.pk)
|
||||||
|
.arg("preshared-key")
|
||||||
|
.arg("/dev/stdin")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.args(&owg.extra_params)
|
||||||
|
.spawn()?;
|
||||||
|
b64_writer(child.stdin.unwrap()).write_all(key.secret())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll(&mut self, rx_buf: &mut [u8]) -> Result<AppPollResult> {
|
||||||
|
use rosenpass::protocol::PollResult as C;
|
||||||
|
use AppPollResult as A;
|
||||||
|
loop {
|
||||||
|
return Ok(match self.crypt.poll()? {
|
||||||
|
C::DeleteKey(PeerPtr(no)) => A::DeleteKey(AppPeerPtr(no)),
|
||||||
|
C::SendInitiation(PeerPtr(no)) => A::SendInitiation(AppPeerPtr(no)),
|
||||||
|
C::SendRetransmission(PeerPtr(no)) => A::SendRetransmission(AppPeerPtr(no)),
|
||||||
|
C::Sleep(timeout) => match self.try_recv(rx_buf, timeout)? {
|
||||||
|
Some((len, addr)) => A::ReceivedMessage(len, addr),
|
||||||
|
None => continue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_recv(&self, buf: &mut [u8], timeout: Timing) -> Result<Option<(usize, SocketAddr)>> {
|
||||||
|
if timeout == 0.0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
self.sock
|
||||||
|
.set_read_timeout(Some(Duration::from_secs_f64(timeout)))?;
|
||||||
|
match self.sock.recv_from(buf) {
|
||||||
|
Ok(x) => Ok(Some(x)),
|
||||||
|
Err(e) => match e.kind() {
|
||||||
|
ErrorKind::WouldBlock => Ok(None),
|
||||||
|
ErrorKind::TimedOut => Ok(None),
|
||||||
|
_ => Err(anyhow::Error::new(e)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
384
src/msgs.rs
Normal file
384
src/msgs.rs
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
//! # Messages
|
||||||
|
//!
|
||||||
|
//! This module contains data structures that help in the
|
||||||
|
//! serialization/deserialization (ser/de) of messages. Thats kind of a lie,
|
||||||
|
//! since no actual ser/de happens. Instead, the structures offer views into
|
||||||
|
//! mutable byte slices (`&mut [u8]`), allowing to modify the fields of an
|
||||||
|
//! always serialized instance of the data in question. This is closely related
|
||||||
|
//! to the concept of lenses in function programming; more on that here:
|
||||||
|
//! [https://sinusoid.es/misc/lager/lenses.pdf](https://sinusoid.es/misc/lager/lenses.pdf)
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! The following example uses the [`data_lense` macro](crate::data_lense) to create a lense that
|
||||||
|
//! might be useful when dealing with UDP headers.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use rosenpass::{data_lense, RosenpassError, msgs::LenseView};
|
||||||
|
//! # fn main() -> Result<(), RosenpassError> {
|
||||||
|
//!
|
||||||
|
//! data_lense! {UdpDatagramHeader :=
|
||||||
|
//! source_port: 2,
|
||||||
|
//! dest_port: 2,
|
||||||
|
//! length: 2,
|
||||||
|
//! checksum: 2
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! let mut buf = [0u8; 8];
|
||||||
|
//!
|
||||||
|
//! // read-only lense, no check of size:
|
||||||
|
//! let lense = UdpDatagramHeader(&buf);
|
||||||
|
//! assert_eq!(lense.checksum(), &[0, 0]);
|
||||||
|
//!
|
||||||
|
//! // mutable lense, runtime check of size
|
||||||
|
//! let mut lense = buf.as_mut().udp_datagram_header()?;
|
||||||
|
//! lense.source_port_mut().copy_from_slice(&53u16.to_be_bytes()); // some DNS, anyone?
|
||||||
|
//!
|
||||||
|
//! // the original buffer is still available
|
||||||
|
//! assert_eq!(buf, [0, 53, 0, 0, 0, 0, 0, 0]);
|
||||||
|
//!
|
||||||
|
//! // read-only lense, runtime check of size
|
||||||
|
//! let lense = buf.as_ref().udp_datagram_header()?;
|
||||||
|
//! assert_eq!(lense.source_port(), &[0, 53]);
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use super::RosenpassError;
|
||||||
|
use crate::{pqkem::*, sodium};
|
||||||
|
|
||||||
|
// Macro magic ////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/// A macro to create data lenses. Refer to the [`msgs` mod](crate::msgs) for
|
||||||
|
/// an example and further elaboration
|
||||||
|
// TODO implement TryFrom<[u8]> and From<[u8; Self::len()]>
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! data_lense(
|
||||||
|
// prefix @ offset ; optional meta ; field name : field length, ...
|
||||||
|
(token_muncher_ref @ $offset:expr ; $( $attr:meta )* ; $field:ident : $len:expr $(, $( $tail:tt )+ )?) => {
|
||||||
|
::paste::paste!{
|
||||||
|
|
||||||
|
#[allow(rustdoc::broken_intra_doc_links)]
|
||||||
|
$( #[ $attr ] )*
|
||||||
|
///
|
||||||
|
#[doc = data_lense!(maybe_docstring_link $len)]
|
||||||
|
/// bytes long
|
||||||
|
pub fn $field(&self) -> &__ContainerType::Output {
|
||||||
|
&self.0[$offset .. $offset + $len]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bytes until the
|
||||||
|
#[doc = data_lense!(maybe_docstring_link Self::$field)]
|
||||||
|
/// field
|
||||||
|
pub fn [< until_ $field >](&self) -> &__ContainerType::Output {
|
||||||
|
&self.0[0 .. $offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the tail exits, consume it as well
|
||||||
|
$(
|
||||||
|
data_lense!{token_muncher_ref @ $offset + $len ; $( $tail )+ }
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// prefix @ offset ; optional meta ; field name : field length, ...
|
||||||
|
(token_muncher_mut @ $offset:expr ; $( $attr:meta )* ; $field:ident : $len:expr $(, $( $tail:tt )+ )?) => {
|
||||||
|
::paste::paste!{
|
||||||
|
|
||||||
|
#[allow(rustdoc::broken_intra_doc_links)]
|
||||||
|
$( #[ $attr ] )*
|
||||||
|
///
|
||||||
|
#[doc = data_lense!(maybe_docstring_link $len)]
|
||||||
|
/// bytes long
|
||||||
|
pub fn [< $field _mut >](&mut self) -> &mut __ContainerType::Output {
|
||||||
|
&mut self.0[$offset .. $offset + $len]
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the tail exits, consume it as well
|
||||||
|
$(
|
||||||
|
data_lense!{token_muncher_mut @ $offset + $len ; $( $tail )+ }
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// switch that yields literals unchanged, but creates docstring links to
|
||||||
|
// constants
|
||||||
|
// TODO the doc string link doesn't work if $x is taken from a generic,
|
||||||
|
(maybe_docstring_link $x:literal) => (stringify!($x));
|
||||||
|
(maybe_docstring_link $x:expr) => (stringify!([$x]));
|
||||||
|
|
||||||
|
// struct name < optional generics > := optional doc string field name : field length, ...
|
||||||
|
($type:ident $( < $( $generic:ident ),+ > )? := $( $( #[ $attr:meta ] )* $field:ident : $len:expr ),+) => (::paste::paste!{
|
||||||
|
|
||||||
|
#[allow(rustdoc::broken_intra_doc_links)]
|
||||||
|
/// A data lense to manipulate byte slices.
|
||||||
|
///
|
||||||
|
//// # Fields
|
||||||
|
///
|
||||||
|
$(
|
||||||
|
/// - `
|
||||||
|
#[doc = stringify!($field)]
|
||||||
|
/// `:
|
||||||
|
#[doc = data_lense!(maybe_docstring_link $len)]
|
||||||
|
/// bytes
|
||||||
|
)+
|
||||||
|
pub struct $type<__ContainerType $(, $( $generic ),+ )? > (
|
||||||
|
__ContainerType,
|
||||||
|
// The phantom data is required, since all generics declared on a
|
||||||
|
// type need to be used on the type.
|
||||||
|
// https://doc.rust-lang.org/stable/error_codes/E0392.html
|
||||||
|
$( $( ::core::marker::PhantomData<$generic> ),+ )?
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<__ContainerType $(, $( $generic: LenseView ),+ )? > $type<__ContainerType $(, $( $generic ),+ )? >{
|
||||||
|
$(
|
||||||
|
/// Size in bytes of the field `
|
||||||
|
#[doc = !($field)]
|
||||||
|
/// `
|
||||||
|
pub const fn [< $field _len >]() -> usize{
|
||||||
|
$len
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
|
||||||
|
/// Verify that `len` is sufficiently long to hold [Self]
|
||||||
|
pub fn check_size(len: usize) -> Result<(), RosenpassError>{
|
||||||
|
let required_size = $( $len + )+ 0;
|
||||||
|
let actual_size = len;
|
||||||
|
if required_size < actual_size {
|
||||||
|
Err(RosenpassError::BufferSizeMismatch {
|
||||||
|
required_size,
|
||||||
|
actual_size,
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read-only accessor functions
|
||||||
|
impl<'a, __ContainerType $(, $( $generic: LenseView ),+ )?> $type<&'a __ContainerType $(, $( $generic ),+ )?>
|
||||||
|
where
|
||||||
|
__ContainerType: std::ops::Index<std::ops::Range<usize>> + ?Sized,
|
||||||
|
{
|
||||||
|
data_lense!{token_muncher_ref @ 0 ; $( $( $attr )* ; $field : $len ),+ }
|
||||||
|
|
||||||
|
/// View into all bytes belonging to this Lense
|
||||||
|
pub fn all_bytes(&self) -> &__ContainerType::Output {
|
||||||
|
&self.0[0..Self::LEN]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mutable accessor functions
|
||||||
|
impl<'a, __ContainerType $(, $( $generic: LenseView ),+ )?> $type<&'a mut __ContainerType $(, $( $generic ),+ )?>
|
||||||
|
where
|
||||||
|
__ContainerType: std::ops::IndexMut<std::ops::Range<usize>> + ?Sized,
|
||||||
|
{
|
||||||
|
data_lense!{token_muncher_ref @ 0 ; $( $( $attr )* ; $field : $len ),+ }
|
||||||
|
data_lense!{token_muncher_mut @ 0 ; $( $( $attr )* ; $field : $len ),+ }
|
||||||
|
|
||||||
|
/// View into all bytes belonging to this Lense
|
||||||
|
pub fn all_bytes(&self) -> &__ContainerType::Output {
|
||||||
|
&self.0[0..Self::LEN]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// View into all bytes belonging to this Lense
|
||||||
|
pub fn all_bytes_mut(&mut self) -> &mut __ContainerType::Output {
|
||||||
|
&mut self.0[0..Self::LEN]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lense trait, allowing us to know the implementing lenses size
|
||||||
|
impl<__ContainerType $(, $( $generic: LenseView ),+ )? > LenseView for $type<__ContainerType $(, $( $generic ),+ )? >{
|
||||||
|
/// Number of bytes required to store this type in binary format
|
||||||
|
const LEN: usize = $( $len + )+ 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait to allow checked creation of a lense over
|
||||||
|
/// some byte slice that contains a
|
||||||
|
#[doc = data_lense!(maybe_docstring_link $type)]
|
||||||
|
pub trait [< $type Ext >] {
|
||||||
|
type __ContainerType;
|
||||||
|
|
||||||
|
/// Create a lense to the byte slice
|
||||||
|
fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type<Self::__ContainerType, $( $($generic),+ )? >, RosenpassError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> [< $type Ext >] for &'a [u8] {
|
||||||
|
type __ContainerType = &'a [u8];
|
||||||
|
|
||||||
|
fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type<Self::__ContainerType, $( $($generic),+ )? >, RosenpassError> {
|
||||||
|
Ok($type ( self, $( $( ::core::marker::PhantomData::<$generic> ),+ )? ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> [< $type Ext >] for &'a mut [u8] {
|
||||||
|
type __ContainerType = &'a mut [u8];
|
||||||
|
|
||||||
|
fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type<Self::__ContainerType, $( $($generic),+ )? >, RosenpassError> {
|
||||||
|
Ok($type ( self, $( $( ::core::marker::PhantomData::<$generic> ),+ )? ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Common trait shared by all Lenses
|
||||||
|
pub trait LenseView {
|
||||||
|
const LEN: usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
data_lense! { Envelope<M> :=
|
||||||
|
/// [MsgType] of this message
|
||||||
|
msg_type: 1,
|
||||||
|
/// Reserved for future use
|
||||||
|
reserved: 3,
|
||||||
|
/// The actual Paylod
|
||||||
|
payload: M::LEN,
|
||||||
|
/// Message Authentication Code (mac) over all bytes until (exclusive)
|
||||||
|
/// `mac` itself
|
||||||
|
mac: sodium::MAC_SIZE,
|
||||||
|
/// Currently unused, TODO: do something with this
|
||||||
|
cookie: sodium::MAC_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
data_lense! { InitHello :=
|
||||||
|
/// Randomly generated connection id
|
||||||
|
sidi: 4,
|
||||||
|
/// Kyber 512 Ephemeral Public Key
|
||||||
|
epki: EKEM::PK_LEN,
|
||||||
|
/// Classic McEliece Ciphertext
|
||||||
|
sctr: SKEM::CT_LEN,
|
||||||
|
/// Encryped: 16 byte hash of McEliece initiator static key
|
||||||
|
pidic: sodium::AEAD_TAG_LEN + 32,
|
||||||
|
/// Encrypted TAI64N Time Stamp (against replay attacks)
|
||||||
|
auth: sodium::AEAD_TAG_LEN
|
||||||
|
}
|
||||||
|
|
||||||
|
data_lense! { RespHello :=
|
||||||
|
/// Randomly generated connection id
|
||||||
|
sidr: 4,
|
||||||
|
/// Copied from InitHello
|
||||||
|
sidi: 4,
|
||||||
|
/// Kyber 512 Ephemeral Ciphertext
|
||||||
|
ecti: EKEM::CT_LEN,
|
||||||
|
/// Classic McEliece Ciphertext
|
||||||
|
scti: SKEM::CT_LEN,
|
||||||
|
/// Empty encrypted message (just an auth tag)
|
||||||
|
auth: sodium::AEAD_TAG_LEN,
|
||||||
|
/// Responders handshake state in encrypted form
|
||||||
|
biscuit: BISCUIT_CT_LEN
|
||||||
|
}
|
||||||
|
|
||||||
|
data_lense! { InitConf :=
|
||||||
|
/// Copied from InitHello
|
||||||
|
sidi: 4,
|
||||||
|
/// Copied from RespHello
|
||||||
|
sidr: 4,
|
||||||
|
/// Responders handshake state in encrypted form
|
||||||
|
biscuit: BISCUIT_CT_LEN,
|
||||||
|
/// Empty encrypted message (just an auth tag)
|
||||||
|
auth: sodium::AEAD_TAG_LEN
|
||||||
|
}
|
||||||
|
|
||||||
|
data_lense! { EmptyData :=
|
||||||
|
/// Copied from RespHello
|
||||||
|
sid: 4,
|
||||||
|
/// Nonce
|
||||||
|
ctr: 8,
|
||||||
|
/// Empty encrypted message (just an auth tag)
|
||||||
|
auth: sodium::AEAD_TAG_LEN
|
||||||
|
}
|
||||||
|
|
||||||
|
data_lense! { Biscuit :=
|
||||||
|
/// H(spki) – Ident ifies the initiator
|
||||||
|
pidi: sodium::KEY_SIZE,
|
||||||
|
/// The biscuit number (replay protection)
|
||||||
|
biscuit_no: 12,
|
||||||
|
/// Chaining key
|
||||||
|
ck: sodium::KEY_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
data_lense! { DataMsg :=
|
||||||
|
dummy: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
data_lense! { CookieReply :=
|
||||||
|
dummy: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
|
||||||
|
pub enum MsgType {
|
||||||
|
InitHello = 0x81,
|
||||||
|
RespHello = 0x82,
|
||||||
|
InitConf = 0x83,
|
||||||
|
EmptyData = 0x84,
|
||||||
|
DataMsg = 0x85,
|
||||||
|
CookieReply = 0x86,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for MsgType {
|
||||||
|
type Error = RosenpassError;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
0x81 => MsgType::InitHello,
|
||||||
|
0x82 => MsgType::RespHello,
|
||||||
|
0x83 => MsgType::InitConf,
|
||||||
|
0x84 => MsgType::EmptyData,
|
||||||
|
0x85 => MsgType::DataMsg,
|
||||||
|
0x86 => MsgType::CookieReply,
|
||||||
|
_ => return Err(RosenpassError::InvalidMessageType(value)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// length in bytes of an unencrypted Biscuit (plain text)
|
||||||
|
pub const BISCUIT_PT_LEN: usize = Biscuit::<()>::LEN;
|
||||||
|
|
||||||
|
/// Length in bytes of an encrypted Biscuit (cipher text)
|
||||||
|
pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + sodium::XAEAD_NONCE_LEN + sodium::XAEAD_TAG_LEN;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_constants {
|
||||||
|
use crate::{
|
||||||
|
msgs::{BISCUIT_CT_LEN, BISCUIT_PT_LEN},
|
||||||
|
sodium,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sodium_keysize() {
|
||||||
|
assert_eq!(sodium::KEY_SIZE, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn biscuit_pt_len() {
|
||||||
|
assert_eq!(BISCUIT_PT_LEN, 2 * sodium::KEY_SIZE + 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn biscuit_ct_len() {
|
||||||
|
assert_eq!(
|
||||||
|
BISCUIT_CT_LEN,
|
||||||
|
BISCUIT_PT_LEN + sodium::XAEAD_NONCE_LEN + sodium::XAEAD_TAG_LEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
176
src/pqkem.rs
Normal file
176
src/pqkem.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
//! This module contains Traits and implementations for Key Encapsulation
|
||||||
|
//! Mechanisms (KEM). KEMs are the interface provided by almost all post-quantum
|
||||||
|
//! secure key exchange mechanisms.
|
||||||
|
//!
|
||||||
|
//! Conceptually KEMs are akin to public-key encryption, but instead of encrypting
|
||||||
|
//! arbitrary data, KEMs are limited to the transmission of keys, randomly chosen during
|
||||||
|
//!
|
||||||
|
//! encapsulation.
|
||||||
|
//! The [KEM] Trait describes the basic API offered by a Key Encapsulation
|
||||||
|
//! Mechanism. Two implementations for it are provided, [SKEM] and [EKEM].
|
||||||
|
|
||||||
|
use crate::{RosenpassError, RosenpassMaybeError};
|
||||||
|
|
||||||
|
/// Key Encapsulation Mechanism
|
||||||
|
///
|
||||||
|
/// The KEM interface defines three operations: Key generation, key encapsulation and key
|
||||||
|
/// decapsulation.
|
||||||
|
pub trait KEM {
|
||||||
|
/// Secrete Key length
|
||||||
|
const SK_LEN: usize;
|
||||||
|
/// Public Key length
|
||||||
|
const PK_LEN: usize;
|
||||||
|
/// Ciphertext length
|
||||||
|
const CT_LEN: usize;
|
||||||
|
/// Shared Secret length
|
||||||
|
const SHK_LEN: usize;
|
||||||
|
|
||||||
|
/// Generate a keypair consisting of secret key (`sk`) and public key (`pk`)
|
||||||
|
///
|
||||||
|
/// `keygen() -> sk, pk`
|
||||||
|
fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError>;
|
||||||
|
|
||||||
|
/// From a public key (`pk`), generate a shared key (`shk`, for local use)
|
||||||
|
/// and a cipher text (`ct`, to be sent to the owner of the `pk`).
|
||||||
|
///
|
||||||
|
/// `encaps(pk) -> shk, ct`
|
||||||
|
fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError>;
|
||||||
|
|
||||||
|
/// From a secret key (`sk`) and a cipher text (`ct`) derive a shared key
|
||||||
|
/// (`shk`)
|
||||||
|
///
|
||||||
|
/// `decaps(sk, ct) -> shk`
|
||||||
|
fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A KEM that is secure against Chosen Ciphertext Attacks (CCA).
|
||||||
|
/// In the context of rosenpass this is used for static keys.
|
||||||
|
/// Uses [Classic McEliece](https://classic.mceliece.org/) 460896 from liboqs.
|
||||||
|
///
|
||||||
|
/// Classic McEliece is chosen because of its high security margin and its small
|
||||||
|
/// ciphertexts. The public keys are humongous, but (being static keys) the are never transmitted over
|
||||||
|
/// the wire so this is not a big problem.
|
||||||
|
pub struct SKEM;
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This Trait impl calls unsafe [oqs_sys] functions, that write to byte
|
||||||
|
/// slices only identified using raw pointers. It must be ensured that the raw
|
||||||
|
/// pointers point into byte slices of sufficient length, to avoid UB through
|
||||||
|
/// overwriting of arbitrary data. This is checked in the following code before
|
||||||
|
/// the unsafe calls, and an early return with an Err occurs if the byte slice
|
||||||
|
/// size does not match the required size.
|
||||||
|
///
|
||||||
|
/// __Note__: This requirement is stricter than necessary, it would suffice
|
||||||
|
/// to only check that the buffers are big enough, allowing them to be even
|
||||||
|
/// bigger. However, from a correctness point of view it does not make sense to
|
||||||
|
/// allow bigger buffers.
|
||||||
|
impl KEM for SKEM {
|
||||||
|
const SK_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_secret_key as usize;
|
||||||
|
const PK_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_public_key as usize;
|
||||||
|
const CT_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_ciphertext as usize;
|
||||||
|
const SHK_LEN: usize =
|
||||||
|
oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_shared_secret as usize;
|
||||||
|
|
||||||
|
fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError> {
|
||||||
|
RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?;
|
||||||
|
unsafe {
|
||||||
|
oqs_sys::kem::OQS_KEM_classic_mceliece_460896_keypair(pk.as_mut_ptr(), sk.as_mut_ptr())
|
||||||
|
.to_rg_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError> {
|
||||||
|
RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?;
|
||||||
|
unsafe {
|
||||||
|
oqs_sys::kem::OQS_KEM_classic_mceliece_460896_encaps(
|
||||||
|
ct.as_mut_ptr(),
|
||||||
|
shk.as_mut_ptr(),
|
||||||
|
pk.as_ptr(),
|
||||||
|
)
|
||||||
|
.to_rg_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError> {
|
||||||
|
RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?;
|
||||||
|
unsafe {
|
||||||
|
oqs_sys::kem::OQS_KEM_classic_mceliece_460896_decaps(
|
||||||
|
shk.as_mut_ptr(),
|
||||||
|
ct.as_ptr(),
|
||||||
|
sk.as_ptr(),
|
||||||
|
)
|
||||||
|
.to_rg_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements a KEM that is secure against Chosen Plaintext Attacks (CPA).
|
||||||
|
/// In the context of rosenpass this is used for ephemeral keys.
|
||||||
|
/// Currently the implementation uses
|
||||||
|
/// [Kyber 512](https://openquantumsafe.org/liboqs/algorithms/kem/kyber) from liboqs.
|
||||||
|
///
|
||||||
|
/// This is being used for ephemeral keys; since these are use-once the first post quantum
|
||||||
|
/// wireguard paper claimed that CPA security would be sufficient. Nonetheless we choose kyber
|
||||||
|
/// which provides CCA security since there are no publicly vetted KEMs out there which provide
|
||||||
|
/// only CPA security.
|
||||||
|
pub struct EKEM;
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This Trait impl calls unsafe [oqs_sys] functions, that write to byte
|
||||||
|
/// slices only identified using raw pointers. It must be ensured that the raw
|
||||||
|
/// pointers point into byte slices of sufficient length, to avoid UB through
|
||||||
|
/// overwriting of arbitrary data. This is checked in the following code before
|
||||||
|
/// the unsafe calls, and an early return with an Err occurs if the byte slice
|
||||||
|
/// size does not match the required size.
|
||||||
|
///
|
||||||
|
/// __Note__: This requirement is stricter than necessary, it would suffice
|
||||||
|
/// to only check that the buffers are big enough, allowing them to be even
|
||||||
|
/// bigger. However, from a correctness point of view it does not make sense to
|
||||||
|
/// allow bigger buffers.
|
||||||
|
impl KEM for EKEM {
|
||||||
|
const SK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_secret_key as usize;
|
||||||
|
const PK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_public_key as usize;
|
||||||
|
const CT_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_ciphertext as usize;
|
||||||
|
const SHK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_shared_secret as usize;
|
||||||
|
fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError> {
|
||||||
|
RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?;
|
||||||
|
unsafe {
|
||||||
|
oqs_sys::kem::OQS_KEM_kyber_512_keypair(pk.as_mut_ptr(), sk.as_mut_ptr())
|
||||||
|
.to_rg_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError> {
|
||||||
|
RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?;
|
||||||
|
unsafe {
|
||||||
|
oqs_sys::kem::OQS_KEM_kyber_512_encaps(
|
||||||
|
ct.as_mut_ptr(),
|
||||||
|
shk.as_mut_ptr(),
|
||||||
|
pk.as_ptr(),
|
||||||
|
)
|
||||||
|
.to_rg_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError> {
|
||||||
|
RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?;
|
||||||
|
RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?;
|
||||||
|
unsafe {
|
||||||
|
oqs_sys::kem::OQS_KEM_kyber_512_decaps(
|
||||||
|
shk.as_mut_ptr(),
|
||||||
|
ct.as_ptr(),
|
||||||
|
sk.as_ptr(),
|
||||||
|
)
|
||||||
|
.to_rg_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/prftree.rs
Normal file
107
src/prftree.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
coloring::Secret,
|
||||||
|
sodium::{hmac, hmac_into, KEY_SIZE},
|
||||||
|
},
|
||||||
|
anyhow::Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO Use a proper Dec interface
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PrfTree([u8; KEY_SIZE]);
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PrfTreeBranch([u8; KEY_SIZE]);
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SecretPrfTree(Secret<KEY_SIZE>);
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SecretPrfTreeBranch(Secret<KEY_SIZE>);
|
||||||
|
|
||||||
|
impl PrfTree {
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self([0u8; KEY_SIZE])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dup(self) -> PrfTreeBranch {
|
||||||
|
PrfTreeBranch(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_secret_prf_tree(self) -> SecretPrfTree {
|
||||||
|
SecretPrfTree(Secret::from_slice(&self.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Protocol! Use domain separation to ensure that
|
||||||
|
pub fn mix(self, v: &[u8]) -> Result<Self> {
|
||||||
|
Ok(Self(hmac(&self.0, v)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretPrfTree> {
|
||||||
|
SecretPrfTree::prf_invoc(&self.0, v.secret())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_value(self) -> [u8; KEY_SIZE] {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrfTreeBranch {
|
||||||
|
pub fn mix(&self, v: &[u8]) -> Result<PrfTree> {
|
||||||
|
Ok(PrfTree(hmac(&self.0, v)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretPrfTree> {
|
||||||
|
SecretPrfTree::prf_invoc(&self.0, v.secret())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecretPrfTree {
|
||||||
|
pub fn prf_invoc(k: &[u8], d: &[u8]) -> Result<SecretPrfTree> {
|
||||||
|
let mut r = SecretPrfTree(Secret::zero());
|
||||||
|
hmac_into(r.0.secret_mut(), k, d)?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self(Secret::zero())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dup(self) -> SecretPrfTreeBranch {
|
||||||
|
SecretPrfTreeBranch(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn danger_from_secret(k: Secret<KEY_SIZE>) -> Self {
|
||||||
|
Self(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix(self, v: &[u8]) -> Result<SecretPrfTree> {
|
||||||
|
Self::prf_invoc(self.0.secret(), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretPrfTree> {
|
||||||
|
Self::prf_invoc(self.0.secret(), v.secret())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_secret(self) -> Secret<KEY_SIZE> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_secret_slice(mut self, v: &[u8], dst: &[u8]) -> Result<()> {
|
||||||
|
hmac_into(self.0.secret_mut(), v, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecretPrfTreeBranch {
|
||||||
|
pub fn mix(&self, v: &[u8]) -> Result<SecretPrfTree> {
|
||||||
|
SecretPrfTree::prf_invoc(self.0.secret(), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretPrfTree> {
|
||||||
|
SecretPrfTree::prf_invoc(self.0.secret(), v.secret())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This entire API is not very nice; we need this for biscuits, but
|
||||||
|
// it might be better to extract a special "biscuit"
|
||||||
|
// labeled subkey and reinitialize the chain with this
|
||||||
|
pub fn danger_into_secret(self) -> Secret<KEY_SIZE> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
1601
src/protocol.rs
Normal file
1601
src/protocol.rs
Normal file
File diff suppressed because it is too large
Load Diff
283
src/sodium.rs
Normal file
283
src/sodium.rs
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
use crate::util::*;
|
||||||
|
use anyhow::{ensure, Result};
|
||||||
|
use libsodium_sys as libsodium;
|
||||||
|
use log::trace;
|
||||||
|
use static_assertions::const_assert_eq;
|
||||||
|
use std::os::raw::{c_ulonglong, c_void};
|
||||||
|
use std::ptr::{null as nullptr, null_mut as nullptr_mut};
|
||||||
|
|
||||||
|
pub const AEAD_TAG_LEN: usize = libsodium::crypto_aead_chacha20poly1305_IETF_ABYTES as usize;
|
||||||
|
pub const AEAD_NONCE_LEN: usize = libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize;
|
||||||
|
pub const XAEAD_TAG_LEN: usize = libsodium::crypto_aead_xchacha20poly1305_ietf_ABYTES as usize;
|
||||||
|
pub const XAEAD_NONCE_LEN: usize = libsodium::crypto_aead_xchacha20poly1305_IETF_NPUBBYTES as usize;
|
||||||
|
pub const NONCE0: [u8; libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize] =
|
||||||
|
[0u8; libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize];
|
||||||
|
pub const NOTHING: [u8; 0] = [0u8; 0];
|
||||||
|
pub const KEY_SIZE: usize = 32;
|
||||||
|
pub const MAC_SIZE: usize = 16;
|
||||||
|
|
||||||
|
const_assert_eq!(
|
||||||
|
KEY_SIZE,
|
||||||
|
libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize
|
||||||
|
);
|
||||||
|
const_assert_eq!(KEY_SIZE, libsodium::crypto_generichash_BYTES as usize);
|
||||||
|
|
||||||
|
macro_rules! sodium_call {
|
||||||
|
($name:ident, $($args:expr),*) => { attempt!({
|
||||||
|
ensure!(unsafe{libsodium::$name($($args),*)} > -1,
|
||||||
|
"Error in libsodium's {}.", stringify!($name));
|
||||||
|
Ok(())
|
||||||
|
})};
|
||||||
|
($name:ident) => { sodium_call!($name, ) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn sodium_init() -> Result<()> {
|
||||||
|
trace!("initializing libsodium");
|
||||||
|
sodium_call!(sodium_init)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn sodium_memcmp(a: &[u8], b: &[u8]) -> bool {
|
||||||
|
a.len() == b.len()
|
||||||
|
&& unsafe {
|
||||||
|
let r = libsodium::sodium_memcmp(
|
||||||
|
a.as_ptr() as *const c_void,
|
||||||
|
b.as_ptr() as *const c_void,
|
||||||
|
a.len(),
|
||||||
|
);
|
||||||
|
r == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn sodium_bigint_cmp(a: &[u8], b: &[u8]) -> i32 {
|
||||||
|
assert!(a.len() == b.len());
|
||||||
|
unsafe { libsodium::sodium_compare(a.as_ptr(), b.as_ptr(), a.len()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn sodium_bigint_inc(v: &mut [u8]) {
|
||||||
|
unsafe {
|
||||||
|
libsodium::sodium_increment(v.as_mut_ptr(), v.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn rng(buf: &mut [u8]) {
|
||||||
|
unsafe { libsodium::randombytes_buf(buf.as_mut_ptr() as *mut c_void, buf.len()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn zeroize(buf: &mut [u8]) {
|
||||||
|
unsafe { libsodium::sodium_memzero(buf.as_mut_ptr() as *mut c_void, buf.len()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn aead_enc_into(
|
||||||
|
ciphertext: &mut [u8],
|
||||||
|
key: &[u8],
|
||||||
|
nonce: &[u8],
|
||||||
|
ad: &[u8],
|
||||||
|
plaintext: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
assert!(ciphertext.len() == plaintext.len() + AEAD_TAG_LEN);
|
||||||
|
assert!(key.len() == libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize);
|
||||||
|
assert!(nonce.len() == libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize);
|
||||||
|
let mut clen: u64 = 0;
|
||||||
|
sodium_call!(
|
||||||
|
crypto_aead_chacha20poly1305_ietf_encrypt,
|
||||||
|
ciphertext.as_mut_ptr(),
|
||||||
|
&mut clen,
|
||||||
|
plaintext.as_ptr(),
|
||||||
|
plaintext.len() as c_ulonglong,
|
||||||
|
ad.as_ptr(),
|
||||||
|
ad.len() as c_ulonglong,
|
||||||
|
nullptr(), // nsec is not used
|
||||||
|
nonce.as_ptr(),
|
||||||
|
key.as_ptr()
|
||||||
|
)?;
|
||||||
|
assert!(clen as usize == ciphertext.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn aead_dec_into(
|
||||||
|
plaintext: &mut [u8],
|
||||||
|
key: &[u8],
|
||||||
|
nonce: &[u8],
|
||||||
|
ad: &[u8],
|
||||||
|
ciphertext: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
assert!(ciphertext.len() == plaintext.len() + AEAD_TAG_LEN);
|
||||||
|
assert!(key.len() == libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize);
|
||||||
|
assert!(nonce.len() == libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize);
|
||||||
|
let mut mlen: u64 = 0;
|
||||||
|
sodium_call!(
|
||||||
|
crypto_aead_chacha20poly1305_ietf_decrypt,
|
||||||
|
plaintext.as_mut_ptr(),
|
||||||
|
&mut mlen as *mut c_ulonglong,
|
||||||
|
nullptr_mut(), // nsec is not used
|
||||||
|
ciphertext.as_ptr(),
|
||||||
|
ciphertext.len() as c_ulonglong,
|
||||||
|
ad.as_ptr(),
|
||||||
|
ad.len() as c_ulonglong,
|
||||||
|
nonce.as_ptr(),
|
||||||
|
key.as_ptr()
|
||||||
|
)?;
|
||||||
|
assert!(mlen as usize == plaintext.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn xaead_enc_into(
|
||||||
|
ciphertext: &mut [u8],
|
||||||
|
key: &[u8],
|
||||||
|
nonce: &[u8],
|
||||||
|
ad: &[u8],
|
||||||
|
plaintext: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
assert!(ciphertext.len() == plaintext.len() + XAEAD_NONCE_LEN + XAEAD_TAG_LEN);
|
||||||
|
assert!(key.len() == libsodium::crypto_aead_xchacha20poly1305_IETF_KEYBYTES as usize);
|
||||||
|
let (n, ct) = ciphertext.split_at_mut(XAEAD_NONCE_LEN);
|
||||||
|
n.copy_from_slice(nonce);
|
||||||
|
let mut clen: u64 = 0;
|
||||||
|
sodium_call!(
|
||||||
|
crypto_aead_xchacha20poly1305_ietf_encrypt,
|
||||||
|
ct.as_mut_ptr(),
|
||||||
|
&mut clen,
|
||||||
|
plaintext.as_ptr(),
|
||||||
|
plaintext.len() as c_ulonglong,
|
||||||
|
ad.as_ptr(),
|
||||||
|
ad.len() as c_ulonglong,
|
||||||
|
nullptr(), // nsec is not used
|
||||||
|
nonce.as_ptr(),
|
||||||
|
key.as_ptr()
|
||||||
|
)?;
|
||||||
|
assert!(clen as usize == ct.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn xaead_dec_into(
|
||||||
|
plaintext: &mut [u8],
|
||||||
|
key: &[u8],
|
||||||
|
ad: &[u8],
|
||||||
|
ciphertext: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
assert!(ciphertext.len() == plaintext.len() + XAEAD_NONCE_LEN + XAEAD_TAG_LEN);
|
||||||
|
assert!(key.len() == libsodium::crypto_aead_xchacha20poly1305_IETF_KEYBYTES as usize);
|
||||||
|
let (n, ct) = ciphertext.split_at(XAEAD_NONCE_LEN);
|
||||||
|
let mut mlen: u64 = 0;
|
||||||
|
sodium_call!(
|
||||||
|
crypto_aead_xchacha20poly1305_ietf_decrypt,
|
||||||
|
plaintext.as_mut_ptr(),
|
||||||
|
&mut mlen as *mut c_ulonglong,
|
||||||
|
nullptr_mut(), // nsec is not used
|
||||||
|
ct.as_ptr(),
|
||||||
|
ct.len() as c_ulonglong,
|
||||||
|
ad.as_ptr(),
|
||||||
|
ad.len() as c_ulonglong,
|
||||||
|
n.as_ptr(),
|
||||||
|
key.as_ptr()
|
||||||
|
)?;
|
||||||
|
assert!(mlen as usize == plaintext.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn blake2b_flexible(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> {
|
||||||
|
const KEY_MIN: usize = libsodium::crypto_generichash_KEYBYTES_MIN as usize;
|
||||||
|
const KEY_MAX: usize = libsodium::crypto_generichash_KEYBYTES_MAX as usize;
|
||||||
|
const OUT_MIN: usize = libsodium::crypto_generichash_BYTES_MIN as usize;
|
||||||
|
const OUT_MAX: usize = libsodium::crypto_generichash_BYTES_MAX as usize;
|
||||||
|
assert!(key.is_empty() || (KEY_MIN <= key.len() && key.len() <= KEY_MAX));
|
||||||
|
assert!(OUT_MIN <= out.len() && out.len() <= OUT_MAX);
|
||||||
|
let kptr = match key.len() {
|
||||||
|
// NULL key
|
||||||
|
0 => nullptr(),
|
||||||
|
_ => key.as_ptr(),
|
||||||
|
};
|
||||||
|
sodium_call!(
|
||||||
|
crypto_generichash_blake2b,
|
||||||
|
out.as_mut_ptr(),
|
||||||
|
out.len(),
|
||||||
|
data.as_ptr(),
|
||||||
|
data.len() as c_ulonglong,
|
||||||
|
kptr,
|
||||||
|
key.len()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Use proper streaming hash; for mix_hash too.
|
||||||
|
#[inline]
|
||||||
|
pub fn hash_into(out: &mut [u8], data: &[u8]) -> Result<()> {
|
||||||
|
assert!(out.len() == KEY_SIZE);
|
||||||
|
blake2b_flexible(out, &NOTHING, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn hash(data: &[u8]) -> Result<[u8; KEY_SIZE]> {
|
||||||
|
let mut r = [0u8; KEY_SIZE];
|
||||||
|
hash_into(&mut r, data)?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn mac_into(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> {
|
||||||
|
assert!(out.len() == KEY_SIZE);
|
||||||
|
assert!(key.len() == KEY_SIZE);
|
||||||
|
blake2b_flexible(out, key, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn mac(key: &[u8], data: &[u8]) -> Result<[u8; KEY_SIZE]> {
|
||||||
|
let mut r = [0u8; KEY_SIZE];
|
||||||
|
mac_into(&mut r, key, data)?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn mac16(key: &[u8], data: &[u8]) -> Result<[u8; 16]> {
|
||||||
|
assert!(key.len() == KEY_SIZE);
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
blake2b_flexible(&mut out, key, data)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn hmac_into(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> {
|
||||||
|
// Not bothering with padding; the implementation
|
||||||
|
// uses appropriately sized keys.
|
||||||
|
ensure!(key.len() == KEY_SIZE);
|
||||||
|
|
||||||
|
const IPAD: [u8; KEY_SIZE] = [0x36u8; KEY_SIZE];
|
||||||
|
let mut temp_key = [0u8; KEY_SIZE];
|
||||||
|
temp_key.copy_from_slice(key);
|
||||||
|
xor_into(&mut temp_key, &IPAD);
|
||||||
|
let outer_data = mac(&temp_key, data)?;
|
||||||
|
|
||||||
|
const OPAD: [u8; KEY_SIZE] = [0x5Cu8; KEY_SIZE];
|
||||||
|
temp_key.copy_from_slice(key);
|
||||||
|
xor_into(&mut temp_key, &OPAD);
|
||||||
|
mac_into(out, &temp_key, &outer_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn hmac(key: &[u8], data: &[u8]) -> Result<[u8; KEY_SIZE]> {
|
||||||
|
let mut r = [0u8; KEY_SIZE];
|
||||||
|
hmac_into(&mut r, key, data)?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose a fully random u64
|
||||||
|
pub fn rand_u64() -> u64 {
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
rng(&mut buf);
|
||||||
|
u64::from_le_bytes(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose a random f64 in [0; 1] inclusive; quick and dirty
|
||||||
|
pub fn rand_f64() -> f64 {
|
||||||
|
(rand_u64() as f64) / (u64::MAX as f64)
|
||||||
|
}
|
||||||
51
src/usage.md
Normal file
51
src/usage.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
NAME
|
||||||
|
|
||||||
|
{0} – Perform post-quantum secure key exchanges for wireguard and other services.
|
||||||
|
|
||||||
|
SYNOPSIS
|
||||||
|
|
||||||
|
{0} [ COMMAND ] [ OPTIONS ]... [ ARGS ]...
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
{0} performs cryptographic key exchanges that are secure against quantum-computers and outputs the keys.
|
||||||
|
These keys can then be passed to various services such as wireguard or other vpn services
|
||||||
|
as pre-shared-keys to achieve security against attackers with quantum computers.
|
||||||
|
|
||||||
|
This is a research project and quantum computers are not thought to become practical in less than ten years.
|
||||||
|
If you are not specifically tasked with developing post-quantum secure systems, you probably do not need this tool.
|
||||||
|
|
||||||
|
COMMANDS
|
||||||
|
|
||||||
|
keygen private-key <file-path> public-key <file-path>
|
||||||
|
Generate a keypair to use in the exchange command later. Send the public-key file to your communication partner
|
||||||
|
and keep the private-key file a secret!
|
||||||
|
|
||||||
|
exchange private-key <file-path> public-key <file-path> [ OPTIONS ]... PEER...\n"
|
||||||
|
Start a process to exchange keys with the specified peers. You should specify at least one peer.
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
listen <ip>[:<port>]
|
||||||
|
Instructs {0} to listen on the specified interface and port. By default {0} will listen on all interfaces and select a random port.
|
||||||
|
|
||||||
|
verbose
|
||||||
|
Extra logging
|
||||||
|
|
||||||
|
PEER := peer public-key <file-path> [endpoint <ip>[:<port>]] [preshared-key <file-path>] [outfile <file-path>] [wireguard <dev> <peer> <extra_params>]
|
||||||
|
Instructs {0} to exchange keys with the given peer and write the resulting PSK into the given output file.
|
||||||
|
You must either specify the outfile or wireguard output option.
|
||||||
|
|
||||||
|
endpoint <ip>[:<port>]
|
||||||
|
Specifies the address where the peer can be reached. This will be automatically updated after the first sucessfull
|
||||||
|
key exchange with the peer. If this is unspecified, the peer must initiate the connection.
|
||||||
|
|
||||||
|
preshared-key <file-path>
|
||||||
|
You may specifie a pre-shared key which will be mixied into the final secret.
|
||||||
|
|
||||||
|
outfile <file-path>
|
||||||
|
You may specify a file to write the exchanged keys to. If this option is specified, {0} will
|
||||||
|
write a notification to standard out every time the key is updated.
|
||||||
|
|
||||||
|
wireguard <dev> <peer> <extra_params>
|
||||||
|
This allows you to directly specify a wireguard peer to deploy the pre-shared-key to.
|
||||||
|
You may specify extra parameters you would pass to `wg set` besides the preshared-key parameter which is used by {0}.
|
||||||
|
This makes it possible to add peers entirely from {0}.
|
||||||
123
src/util.rs
Normal file
123
src/util.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use base64::{
|
||||||
|
display::Base64Display as B64Display, read::DecoderReader as B64Reader,
|
||||||
|
write::EncoderWriter as B64Writer,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
borrow::{Borrow, BorrowMut},
|
||||||
|
cmp::min,
|
||||||
|
io::{Read, Write},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn xor_into(a: &mut [u8], b: &[u8]) {
|
||||||
|
assert!(a.len() == b.len());
|
||||||
|
for (av, bv) in a.iter_mut().zip(b.iter()) {
|
||||||
|
*av ^= *bv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Zeroize result?
|
||||||
|
/** Concatenate two byte arrays */
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! cat {
|
||||||
|
($len:expr; $($toks:expr),+) => {{
|
||||||
|
let mut buf = [0u8; $len];
|
||||||
|
let mut off = 0;
|
||||||
|
$({
|
||||||
|
let tok = $toks;
|
||||||
|
let tr = ::std::borrow::Borrow::<[u8]>::borrow(tok);
|
||||||
|
(&mut buf[off..(off + tr.len())]).copy_from_slice(tr);
|
||||||
|
off += tr.len();
|
||||||
|
})+
|
||||||
|
assert!(off == buf.len(), "Size mismatch in cat!()");
|
||||||
|
buf
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: consistent inout ordering
|
||||||
|
pub fn cpy<T: BorrowMut<[u8]> + ?Sized, F: Borrow<[u8]> + ?Sized>(src: &F, dst: &mut T) {
|
||||||
|
dst.borrow_mut().copy_from_slice(src.borrow());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cpy_min<T: BorrowMut<[u8]> + ?Sized, F: Borrow<[u8]> + ?Sized>(src: &F, to: &mut T) {
|
||||||
|
let src = src.borrow();
|
||||||
|
let dst = to.borrow_mut();
|
||||||
|
let len = min(src.len(), dst.len());
|
||||||
|
dst[..len].copy_from_slice(&src[..len]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try block basically…returns a result and allows the use of the question mark operator inside
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! attempt {
|
||||||
|
($block:expr) => {
|
||||||
|
(|| -> ::anyhow::Result<_> { $block })()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const B64TYPE: base64::Config = base64::STANDARD;
|
||||||
|
|
||||||
|
pub fn fmt_b64<'a>(payload: &'a [u8]) -> B64Display<'a> {
|
||||||
|
B64Display::<'a>::with_config(payload, B64TYPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn b64_writer<W: Write>(w: W) -> B64Writer<W> {
|
||||||
|
B64Writer::new(w, B64TYPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn b64_reader<R: Read>(r: &mut R) -> B64Reader<'_, R> {
|
||||||
|
B64Reader::new(r, B64TYPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove this once std::cmp::max becomes const
|
||||||
|
pub const fn max_usize(a: usize, b: usize) -> usize {
|
||||||
|
if a > b {
|
||||||
|
a
|
||||||
|
} else {
|
||||||
|
b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Timebase(Instant);
|
||||||
|
|
||||||
|
impl Default for Timebase {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Instant::now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timebase {
|
||||||
|
pub fn now(&self) -> f64 {
|
||||||
|
self.0.elapsed().as_secs_f64()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dur(&self, t: f64) -> Duration {
|
||||||
|
Duration::from_secs_f64(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! multimatch {
|
||||||
|
($val:expr) => {{ () }};
|
||||||
|
($val:expr, $($p:pat => $thn:expr),*) => {{
|
||||||
|
let v = $val;
|
||||||
|
($(if let $p = v { Some($thn) } else { None }),*)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mutating<T, F>(mut v: T, f: F) -> T
|
||||||
|
where
|
||||||
|
F: Fn(&mut T),
|
||||||
|
{
|
||||||
|
f(&mut v);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sideeffect<T, F>(v: T, f: F) -> T
|
||||||
|
where
|
||||||
|
F: Fn(&T),
|
||||||
|
{
|
||||||
|
f(&v);
|
||||||
|
v
|
||||||
|
}
|
||||||
120
tests/integration_test.rs
Normal file
120
tests/integration_test.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
use std::{fs, net::UdpSocket, path::PathBuf, process::Stdio, time::Duration};
|
||||||
|
|
||||||
|
const BIN: &str = "rosenpass";
|
||||||
|
|
||||||
|
// check that we can generate keys
|
||||||
|
#[test]
|
||||||
|
fn generate_keys() {
|
||||||
|
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("keygen");
|
||||||
|
fs::create_dir_all(&tmpdir).unwrap();
|
||||||
|
|
||||||
|
let priv_key_path = tmpdir.join("private-key");
|
||||||
|
let pub_key_path = tmpdir.join("public-key");
|
||||||
|
|
||||||
|
let output = test_bin::get_test_bin(BIN)
|
||||||
|
.args(["keygen", "private-key"])
|
||||||
|
.arg(&priv_key_path)
|
||||||
|
.arg("public-key")
|
||||||
|
.arg(&pub_key_path)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to start {BIN}");
|
||||||
|
|
||||||
|
assert_eq!(String::from_utf8_lossy(&output.stdout), "");
|
||||||
|
|
||||||
|
assert!(priv_key_path.is_file());
|
||||||
|
assert!(pub_key_path.is_file());
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
fs::remove_dir_all(&tmpdir).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_udp_socket() -> u16 {
|
||||||
|
for port in 1025..=u16::MAX {
|
||||||
|
match UdpSocket::bind(("127.0.0.1", port)) {
|
||||||
|
Ok(_) => {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("no free UDP port found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we can exchange keys
|
||||||
|
#[test]
|
||||||
|
fn check_exchange() {
|
||||||
|
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange");
|
||||||
|
fs::create_dir_all(&tmpdir).unwrap();
|
||||||
|
|
||||||
|
let priv_key_paths = [tmpdir.join("private-key-0"), tmpdir.join("private-key-1")];
|
||||||
|
let pub_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")];
|
||||||
|
let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")];
|
||||||
|
|
||||||
|
// generate key pairs
|
||||||
|
for (priv_key_path, pub_key_path) in priv_key_paths.iter().zip(pub_key_paths.iter()) {
|
||||||
|
let output = test_bin::get_test_bin(BIN)
|
||||||
|
.args(["keygen", "private-key"])
|
||||||
|
.arg(&priv_key_path)
|
||||||
|
.arg("public-key")
|
||||||
|
.arg(&pub_key_path)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to start {BIN}");
|
||||||
|
|
||||||
|
assert_eq!(String::from_utf8_lossy(&output.stdout), "");
|
||||||
|
assert!(priv_key_path.is_file());
|
||||||
|
assert!(pub_key_path.is_file());
|
||||||
|
}
|
||||||
|
|
||||||
|
// start first process, the server
|
||||||
|
let port = find_udp_socket();
|
||||||
|
let listen_addr = format!("localhost:{port}");
|
||||||
|
let mut server = test_bin::get_test_bin(BIN)
|
||||||
|
.args(["exchange", "private-key"])
|
||||||
|
.arg(&priv_key_paths[0])
|
||||||
|
.arg("public-key")
|
||||||
|
.arg(&pub_key_paths[0])
|
||||||
|
.args(["listen", &listen_addr, "verbose", "peer", "public-key"])
|
||||||
|
.arg(&pub_key_paths[1])
|
||||||
|
.arg("outfile")
|
||||||
|
.arg(&shared_key_paths[0])
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to start {BIN}");
|
||||||
|
|
||||||
|
// start second process, the client
|
||||||
|
let mut client = test_bin::get_test_bin(BIN)
|
||||||
|
.args(["exchange", "private-key"])
|
||||||
|
.arg(&priv_key_paths[1])
|
||||||
|
.arg("public-key")
|
||||||
|
.arg(&pub_key_paths[1])
|
||||||
|
.args(["verbose", "peer", "public-key"])
|
||||||
|
.arg(&pub_key_paths[0])
|
||||||
|
.args(["endpoint", &listen_addr])
|
||||||
|
.arg("outfile")
|
||||||
|
.arg(&shared_key_paths[1])
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to start {BIN}");
|
||||||
|
|
||||||
|
// give them some time to do the key exchange
|
||||||
|
std::thread::sleep(Duration::from_secs(2));
|
||||||
|
|
||||||
|
// time's up, kill the childs
|
||||||
|
server.kill().unwrap();
|
||||||
|
client.kill().unwrap();
|
||||||
|
|
||||||
|
// read the shared keys they created
|
||||||
|
let shared_keys: Vec<_> = shared_key_paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| fs::read_to_string(p).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// check that they created two equal keys
|
||||||
|
assert_eq!(shared_keys.len(), 2);
|
||||||
|
assert_eq!(shared_keys[0], shared_keys[1]);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
fs::remove_dir_all(&tmpdir).unwrap();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user