Rewrite vault in Rust (format-compatible with 0.5) #1
@@ -0,0 +1,102 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Oracle-backed interoperability tests must run, not skip, in CI.
|
||||
VAULT_REQUIRE_ORACLE: "1"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: rustup show active-toolchain || rustup toolchain install
|
||||
- run: cargo fmt --check
|
||||
- run: cargo clippy --locked --all-targets --all-features -- -D warnings
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel, windows-2025]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
# The Go toolchain builds the test-only oracle; vault itself needs no Go.
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache-dependency-path: oracle/go/go.sum
|
||||
- run: rustup show active-toolchain || rustup toolchain install
|
||||
- name: test
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ $RUNNER_OS == Windows ]]; then unset VAULT_REQUIRE_ORACLE; fi
|
||||
cargo test --locked --all-targets --all-features
|
||||
- run: cargo test --locked --doc
|
||||
- run: cargo build --locked --release
|
||||
- name: oracle self-test
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: oracle/go
|
||||
run: go test ./...
|
||||
|
||||
cross:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: rustup show active-toolchain || rustup toolchain install
|
||||
- name: zig and cargo-zigbuild
|
||||
run: |
|
||||
curl -sSLo zig.tar.xz https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
|
||||
echo "70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 zig.tar.xz" | sha256sum -c
|
||||
mkdir -p "$HOME/zig" && tar -xJf zig.tar.xz -C "$HOME/zig" --strip-components=1
|
||||
echo "$HOME/zig" >>"$GITHUB_PATH"
|
||||
cargo install cargo-zigbuild --version 0.23.4 --locked
|
||||
- name: targets
|
||||
run: |
|
||||
rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl i686-unknown-linux-musl \
|
||||
arm-unknown-linux-musleabihf armv7-unknown-linux-musleabihf riscv64gc-unknown-linux-musl \
|
||||
loongarch64-unknown-linux-musl powerpc64le-unknown-linux-musl powerpc64-unknown-linux-gnu \
|
||||
s390x-unknown-linux-gnu x86_64-pc-windows-gnu i686-pc-windows-gnu aarch64-pc-windows-gnullvm wasm32-wasip1
|
||||
- name: build
|
||||
run: |
|
||||
TARGETS="linux/amd64 linux/arm64 linux/386 linux/armv6 linux/armv7 linux/riscv64 linux/loong64 \
|
||||
linux/ppc64le linux/ppc64 linux/s390x windows/amd64 windows/386 windows/arm64 wasip1/wasm" ./build.sh
|
||||
- name: run the arm64 and big-endian s390x builds under qemu
|
||||
run: |
|
||||
sudo apt-get update -q && sudo apt-get install -yq qemu-user
|
||||
cargo build --locked --release
|
||||
EMU=qemu-aarch64 scripts/smoke.sh dist/vault-linux-arm64 target/release/vault
|
||||
EMU="qemu-s390x -L /usr/s390x-linux-gnu" scripts/smoke.sh dist/vault-linux-s390x target/release/vault || echo "s390x needs a glibc sysroot; build-only"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
supply-chain:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: rustup show active-toolchain || rustup toolchain install
|
||||
- run: cargo install cargo-deny --version 0.20.2 --locked
|
||||
- run: cargo deny --locked check
|
||||
|
||||
fuzz:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: rustup toolchain install nightly-2026-10-01 --profile minimal
|
||||
- run: cargo +nightly-2026-10-01 install cargo-fuzz --version 0.13.2 --locked
|
||||
- name: a short run of every target
|
||||
working-directory: fuzz
|
||||
run: |
|
||||
for t in $(cargo +nightly-2026-10-01 fuzz list); do
|
||||
cargo +nightly-2026-10-01 fuzz run "$t" -- -max_total_time=60
|
||||
done
|
||||
@@ -7,3 +7,7 @@
|
||||
/docs/guide/vault-guide.pdf
|
||||
/docs/guide/*.log
|
||||
/target
|
||||
fuzz/target
|
||||
fuzz/corpus
|
||||
fuzz/artifacts
|
||||
fuzz/coverage
|
||||
|
||||
@@ -35,3 +35,8 @@ rustix = { version = "1.1.5", features = ["fs", "process", "mm", "termios", "eve
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_Security"] }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
strip = true
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
# Security notes
|
||||
|
||||
What vault protects, how, and where the protection stops. See
|
||||
`docs/COMPATIBILITY.md` for the exact formats.
|
||||
|
||||
## What is protected
|
||||
|
||||
- **Contents, names, sizes, modes and times** of stored files, against
|
||||
anyone who reads the vault directory without the passphrase: everything is
|
||||
inside Fury files sealed to a hybrid FrodoKEM-640 + ML-KEM-768 key, so the
|
||||
file key stays secret unless both KEMs are broken.
|
||||
- **Structure**: records are one fixed size; objects are at most 4 MiB and
|
||||
padded to power-of-two buckets; object names derive from a secret per-file
|
||||
seed, so nothing on disk links a file's objects; every ciphertext and shard
|
||||
directory has its timestamps pinned to the epoch; all 256 shards exist from
|
||||
the start; `init` adds filler sealed to a discarded key.
|
||||
- **Integrity**: every byte that is acted on is authenticated (header MAC,
|
||||
STREAM tags with a final-chunk flag, no trailing data); an object starts
|
||||
with its own ID, so objects cannot be swapped, reordered or replayed under
|
||||
another record; `get` publishes output only after the whole file, padding
|
||||
and end of file included, has authenticated.
|
||||
- **Deniability**: the key file always has two equal slots, one possibly
|
||||
random; both are always tried; decoy and real files are indistinguishable
|
||||
from filler to the other passphrase.
|
||||
|
||||
## What is not protected
|
||||
|
||||
- The number and total size of ciphertexts, and when the directory changes,
|
||||
are visible to whoever stores it.
|
||||
- A passphrase is only as strong as it is: Argon2id (3 passes, 64 MiB, 4
|
||||
lanes) slows guessing, it does not prevent it.
|
||||
- Anyone who can run code as you while the vault is unlocked (or watch you
|
||||
type the passphrase) can read everything.
|
||||
- Nothing here hides that you use vault.
|
||||
|
||||
## Secrets in memory
|
||||
|
||||
Passphrases, seeds, private keys, key-encryption keys and plaintext buffers
|
||||
are held in owned types (`SecretArray`, `SecretVec`, `LockedBuf`) that are
|
||||
not `Clone`, `Copy`, `Debug` or serialisable, have fixed allocations that
|
||||
never reallocate, and are cleared with `zeroize` when dropped — on success,
|
||||
on error and while unwinding from a panic (release builds unwind).
|
||||
|
||||
What cannot be cleared, so is not claimed:
|
||||
|
||||
- copies the compiler leaves in registers or on the stack;
|
||||
- copies inside third-party key objects: `chacha20poly1305` cipher states
|
||||
(the key schedule), `ml-kem`'s `Seed` passed by value (the decapsulation
|
||||
key itself is zeroized on drop with the crate's `zeroize` feature),
|
||||
`hmac`/`hkdf` internal states, `argon2` internals other than its working
|
||||
memory (which vault allocates and clears itself);
|
||||
- the passphrase in `$VAULT_PASSPHRASE` (it is in the process environment
|
||||
for the process lifetime) and in the terminal driver's buffers;
|
||||
- anything after `abort`, `kill -9`, a power cut, or once swapped out;
|
||||
- file data in the OS page cache, and plaintext temporary files on disk
|
||||
(below).
|
||||
|
||||
None of this is erasure from physical memory.
|
||||
|
||||
## The agent
|
||||
|
||||
`vault unlock` re-executes the binary as `vault __agent` with
|
||||
`VAULT_PASSPHRASE` and `VAULT_DECOY_PASSPHRASE` removed from its environment,
|
||||
no secret in its arguments, its working directory `/` and standard error
|
||||
closed. It hands over the 34-byte suite ID and seed through a pipe. Before
|
||||
reading it, the agent:
|
||||
|
||||
1. sets the core-dump size limit to zero;
|
||||
2. on Linux, sets `PR_SET_DUMPABLE` to 0 (no core dumps, no same-user
|
||||
`ptrace`, `/proc/PID/mem` or `/proc/PID/environ`) and checks it took;
|
||||
3. maps a private page, `mlock`s it and, on Linux, marks it
|
||||
`MADV_DONTDUMP`, all checked.
|
||||
|
||||
Any failure is reported to `unlock` and the agent exits without the key;
|
||||
there is no fallback to unprotected memory. On other Unix systems (macOS,
|
||||
the BSDs, illumos) steps 1 and 3 apply; there is no portable equivalent of
|
||||
`PR_SET_DUMPABLE`, so another process of the same user may be able to read
|
||||
the agent's memory with a debugger.
|
||||
|
||||
The socket lives in a directory that must be owned by you with mode exactly
|
||||
0700 (refused otherwise); the socket is 0600, checked before readiness. Each
|
||||
request has a two-second deadline; the expiry deadline is enforced with a
|
||||
timed `poll`, so it does not depend on another client arriving, and a key
|
||||
is never handed out after expiry. On exit the agent clears the page and
|
||||
removes the socket only if it is still its own. `unlock` waits at most ten
|
||||
seconds for the agent to report and kills it otherwise.
|
||||
|
||||
Windows: the agent is not supported (`unlock` says so); every command asks
|
||||
for the passphrase.
|
||||
|
||||
## Filesystem safety
|
||||
|
||||
- Output is published with an atomic no-replace primitive
|
||||
(`renameat2(RENAME_NOREPLACE)`, `renameatx_np(RENAME_EXCL)`, `link`+`unlink`,
|
||||
or `MoveFileExW` without replace), so an existing file, symlink (dangling
|
||||
or not) or one created concurrently is never replaced; only vault's own
|
||||
temporary file is removed. There is no check-then-rename fallback.
|
||||
- The temporary output file is mode 0600 until the content has fully
|
||||
authenticated; only then are the recorded mode and times applied.
|
||||
- `get -r` opens each directory below the chosen destination with
|
||||
`O_NOFOLLOW|O_DIRECTORY` relative to its parent, so a symlink planted
|
||||
inside the destination cannot redirect files elsewhere. On Windows, which
|
||||
has no `openat`, each component is checked not to be a link or junction
|
||||
just before use; a concurrent swap between check and use is not excluded.
|
||||
- Writes follow crash-safe order with explicit `fsync` of files and
|
||||
directories (Unix): objects before their record; a new record before the
|
||||
removal of the one it replaces; a record's removal before its objects'.
|
||||
A failure after a record is already in place never deletes its objects.
|
||||
Windows has no directory fsync through the APIs used; NTFS journals
|
||||
metadata.
|
||||
- The vault lock uses the same POSIX `fcntl` locks as the Go version, so
|
||||
mixed Go and Rust processes exclude each other. Only a read-only
|
||||
filesystem lets a reader proceed without the lock.
|
||||
|
||||
## Plaintext on disk
|
||||
|
||||
- `vault add URL` and `vault mirror` stream non-HTML downloads to a private
|
||||
(0600) temporary file in `$TMPDIR` before storing them, because the size
|
||||
must be known first; it is removed on every normal path, including
|
||||
errors. HTML and CSS are rewritten in memory. `vault absorb` stores tar
|
||||
entries straight from the stream with no temporary file.
|
||||
- `vault get` writes plaintext where you ask; `vault migrate` deletes the
|
||||
originals with `unlink`, which is not secure deletion.
|
||||
|
||||
## Network
|
||||
|
||||
Downloads verify TLS certificates (rustls with the platform verifier; it is
|
||||
never disabled), follow at most 10 redirects (http and https only; for
|
||||
`mirror`, only to the same host), and are bounded by connect, response and
|
||||
whole-request deadlines and a byte budget counted after decompression. See
|
||||
`vault(1)` for the defaults and flags.
|
||||
|
||||
## Cryptographic implementations
|
||||
|
||||
FrodoKEM-640-SHAKE is a port of CIRCL v1.6.4's implementation (constant-time
|
||||
in the same places: no secret-dependent branches or indices; the
|
||||
re-encryption check selects the rejection key with a mask). It reproduces
|
||||
CIRCL's NIST KAT digest. ML-KEM-768 (`ml-kem`), ChaCha20-Poly1305 and
|
||||
XChaCha20-Poly1305 (`chacha20poly1305`), Argon2id (`argon2`), HKDF, HMAC,
|
||||
SHA-2 and SHA-3 are RustCrypto crates; TLS is rustls with `ring`. A Rust
|
||||
rewrite is not stronger cryptography: the algorithms and parameters are
|
||||
unchanged.
|
||||
|
||||
## Reporting
|
||||
|
||||
Please report vulnerabilities privately to andy@dixon.cx.
|
||||
@@ -1,48 +1,143 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build vault for every OS and CPU architecture Go can target without cgo.
|
||||
# Build vault for the supported operating systems and CPU architectures.
|
||||
#
|
||||
# ./build.sh # everything, into ./dist
|
||||
# ./build.sh # every target this host can build, into ./dist
|
||||
# TARGETS="linux/amd64 darwin/arm64" ./build.sh
|
||||
# OUT=/tmp/release ./build.sh
|
||||
# OUT=/tmp/release JOBS=4 ./build.sh
|
||||
#
|
||||
# Binaries are static, stripped and reproducible (CGO_ENABLED=0, -trimpath).
|
||||
# iOS and most Android targets are skipped (they need cgo); see below.
|
||||
# 32-bit ARM Linux is built twice: armv6 (Raspberry Pi Zero/1) and armv7.
|
||||
# SHA256SUMS is written next to the binaries.
|
||||
# Binaries keep the Go-era names, vault-OS-ARCH[.exe], and are stripped
|
||||
# release builds from the pinned toolchain (rust-toolchain.toml) and the
|
||||
# locked dependencies (Cargo.lock), with build paths remapped so the output
|
||||
# does not depend on where the tree lives. Linux binaries are static (musl)
|
||||
# except s390x and ppc64, which link glibc dynamically; static linking is
|
||||
# checked. SHA256SUMS is written next to the binaries.
|
||||
#
|
||||
# How each target is built, and what the host needs:
|
||||
# zig cargo-zigbuild and zig on PATH (Linux, Windows GNU ABI)
|
||||
# mac a Mac with Xcode (macOS)
|
||||
# cross `cross` and Docker (FreeBSD, NetBSD, illumos, Solaris)
|
||||
# cargo nothing extra (WASI)
|
||||
# A requested target the host cannot build is reported, the others are still
|
||||
# built, and the script exits non-zero. See docs/PLATFORMS.md for the Go
|
||||
# targets with no Rust equivalent; asking for one is an error.
|
||||
#
|
||||
# JOBS builds several targets at once, each in its own target directory
|
||||
# under target/build-sh (default 1: one at a time, cargo using every CPU).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
root=$PWD
|
||||
|
||||
table() {
|
||||
# os/arch Rust target method
|
||||
cat <<-'EOF'
|
||||
linux/amd64 x86_64-unknown-linux-musl zig
|
||||
linux/arm64 aarch64-unknown-linux-musl zig
|
||||
linux/386 i686-unknown-linux-musl zig
|
||||
linux/armv6 arm-unknown-linux-musleabihf zig
|
||||
linux/armv7 armv7-unknown-linux-musleabihf zig
|
||||
linux/riscv64 riscv64gc-unknown-linux-musl zig
|
||||
linux/loong64 loongarch64-unknown-linux-musl zig
|
||||
linux/ppc64le powerpc64le-unknown-linux-musl zig
|
||||
linux/ppc64 powerpc64-unknown-linux-gnu zig
|
||||
linux/s390x s390x-unknown-linux-gnu zig
|
||||
android/arm64 aarch64-unknown-linux-musl zig
|
||||
windows/amd64 x86_64-pc-windows-gnu zig
|
||||
windows/386 i686-pc-windows-gnu zig
|
||||
windows/arm64 aarch64-pc-windows-gnullvm zig
|
||||
darwin/amd64 x86_64-apple-darwin mac
|
||||
darwin/arm64 aarch64-apple-darwin mac
|
||||
freebsd/amd64 x86_64-unknown-freebsd cross
|
||||
freebsd/386 i686-unknown-freebsd cross
|
||||
netbsd/amd64 x86_64-unknown-netbsd cross
|
||||
illumos/amd64 x86_64-unknown-illumos cross
|
||||
solaris/amd64 x86_64-pc-solaris cross
|
||||
wasip1/wasm wasm32-wasip1 cargo
|
||||
EOF
|
||||
}
|
||||
|
||||
# Go targets that Rust cannot build with a stable toolchain (no prebuilt
|
||||
# standard library, or no Rust target at all). See docs/PLATFORMS.md.
|
||||
unsupported="aix/ppc64 dragonfly/amd64 freebsd/arm freebsd/arm64 freebsd/riscv64 js/wasm
|
||||
linux/mips linux/mipsle linux/mips64 linux/mips64le netbsd/386 netbsd/arm netbsd/arm64
|
||||
openbsd/386 openbsd/amd64 openbsd/arm openbsd/arm64 openbsd/ppc64 openbsd/riscv64
|
||||
plan9/386 plan9/amd64 plan9/arm"
|
||||
|
||||
out=${OUT:-dist}
|
||||
# iOS and 32-bit/x86 Android need cgo and a vendor SDK. android/arm64 builds
|
||||
# a static binary that runs in Termux.
|
||||
targets=${TARGETS:-$(go tool dist list | grep -vE '^ios/|^android/(386|amd64|arm)$')}
|
||||
jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}
|
||||
jobs=${JOBS:-1}
|
||||
targets=${TARGETS:-$(table | awk '{print $1}')}
|
||||
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
# Never clean anything but our own output, and never in a directory that
|
||||
# matters: OUT must be a dedicated directory, not /, $HOME or the tree.
|
||||
mkdir -p -- "$out"
|
||||
out=$(cd -- "$out" && pwd -P)
|
||||
case $out in
|
||||
/ | "$(cd ~ && pwd -P)" | "$root") echo "refusing OUT=$out: use a dedicated directory" >&2; exit 2 ;;
|
||||
esac
|
||||
rm -f -- "$out"/vault-* "$out"/SHA256SUMS "$out"/*.log
|
||||
|
||||
export CARGO_TARGET_DIR_BASE=$root/target/build-sh
|
||||
export RUSTFLAGS="${RUSTFLAGS:-} --remap-path-prefix=$root=vault --remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=cargo"
|
||||
export OUT_DIR=$out JOBS_N=$jobs HOST_OS=$(uname -s)
|
||||
|
||||
build_one() {
|
||||
local os=${1%/*} arch=${1#*/} variants=("") name ext=""
|
||||
[[ $os == windows ]] && ext=.exe
|
||||
[[ $arch == wasm ]] && ext=.wasm
|
||||
[[ $os/$arch == linux/arm ]] && variants=(6 7)
|
||||
for v in "${variants[@]}"; do
|
||||
name=vault-$os-$arch${v:+v$v}$ext
|
||||
if ! GOOS=$os GOARCH=$arch GOARM=${v:-} CGO_ENABLED=0 \
|
||||
go build -trimpath -buildvcs=false -ldflags='-s -w -buildid=' -o "$OUT_DIR/$name" . 2>"$OUT_DIR/$name.log"; then
|
||||
echo "FAILED $name" >&2
|
||||
cat "$OUT_DIR/$name.log" >&2
|
||||
return 1
|
||||
local want=$1 row os triple method name ext="" tdir bin
|
||||
row=$(table | awk -v t="$want" '$1 == t')
|
||||
if [[ -z $row ]]; then
|
||||
if grep -qw -- "$want" <<<"$unsupported"; then
|
||||
echo "UNSUPPORTED $want: no stable Rust target; see docs/PLATFORMS.md" >&2
|
||||
else
|
||||
echo "UNKNOWN $want" >&2
|
||||
fi
|
||||
rm "$OUT_DIR/$name.log"
|
||||
echo "built $name"
|
||||
done
|
||||
return 1
|
||||
fi
|
||||
read -r _ triple method <<<"$row"
|
||||
os=${want%/*}
|
||||
[[ $os == windows ]] && ext=.exe
|
||||
[[ $os == wasip1 ]] && ext=.wasm
|
||||
name=vault-${want/\//-}$ext
|
||||
tdir=$root/target
|
||||
[[ $JOBS_N != 1 ]] && tdir=$CARGO_TARGET_DIR_BASE/$triple
|
||||
case $method in
|
||||
zig)
|
||||
command -v cargo-zigbuild >/dev/null && command -v zig >/dev/null ||
|
||||
{ echo "SKIPPED $want: needs cargo-zigbuild and zig" >&2; return 1; }
|
||||
cmd=(cargo zigbuild) ;;
|
||||
mac)
|
||||
[[ $HOST_OS == Darwin ]] || { echo "SKIPPED $want: needs a Mac with Xcode" >&2; return 1; }
|
||||
cmd=(cargo build) ;;
|
||||
cross)
|
||||
command -v cross >/dev/null && command -v docker >/dev/null ||
|
||||
{ echo "SKIPPED $want: needs cross and docker" >&2; return 1; }
|
||||
cmd=(cross build) ;;
|
||||
cargo) cmd=(cargo build) ;;
|
||||
esac
|
||||
local flags=$RUSTFLAGS
|
||||
# zig's musl getrandom() never reaches the kernel on riscv64 (it spins);
|
||||
# make the getrandom crate issue the system call itself.
|
||||
[[ $triple == riscv64*-musl ]] && flags+=' --cfg getrandom_backend="linux_raw"'
|
||||
if ! RUSTFLAGS=$flags CARGO_TARGET_DIR=$tdir "${cmd[@]}" --release --locked --target "$triple" >"$OUT_DIR/$name.log" 2>&1; then
|
||||
echo "FAILED $want ($triple)" >&2
|
||||
tail -20 "$OUT_DIR/$name.log" >&2
|
||||
return 1
|
||||
fi
|
||||
bin=$tdir/$triple/release/vault$ext
|
||||
[[ $os == wasip1 ]] && bin=$tdir/$triple/release/vault.wasm
|
||||
cp -- "$bin" "$OUT_DIR/$name"
|
||||
rm -f -- "$OUT_DIR/$name.log"
|
||||
if [[ $triple == *-musl* ]] && command -v file >/dev/null; then
|
||||
file -L "$OUT_DIR/$name" | grep -q 'static' || { echo "FAILED $want: not statically linked" >&2; return 1; }
|
||||
fi
|
||||
echo "built $name ($triple)"
|
||||
}
|
||||
export -f build_one
|
||||
export OUT_DIR=$out
|
||||
export -f build_one table
|
||||
export unsupported
|
||||
|
||||
printf '%s\n' $targets | xargs -P "$jobs" -I{} bash -c 'build_one {}'
|
||||
status=0
|
||||
printf '%s\n' $targets | xargs -P "$jobs" -I{} bash -c 'build_one {}' || status=1
|
||||
|
||||
cd "$out"
|
||||
if command -v sha256sum >/dev/null; then sha256sum vault-* >SHA256SUMS; else shasum -a 256 vault-* >SHA256SUMS; fi
|
||||
echo "$(ls vault-* | wc -l | tr -d ' ') binaries in $out"
|
||||
cd -- "$out"
|
||||
if ls vault-* >/dev/null 2>&1; then
|
||||
if command -v sha256sum >/dev/null; then sha256sum vault-* >SHA256SUMS; else shasum -a 256 vault-* >SHA256SUMS; fi
|
||||
fi
|
||||
echo "$(ls vault-* 2>/dev/null | wc -l | tr -d ' ') binaries in $out"
|
||||
exit $status
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# cargo-deny policy: run `cargo deny --locked check`.
|
||||
|
||||
[graph]
|
||||
all-features = true
|
||||
|
||||
[advisories]
|
||||
# Every advisory is an error unless listed here with a reason.
|
||||
ignore = []
|
||||
|
||||
[licenses]
|
||||
# Licences compatible with distributing vault under GPL-3.0-or-later.
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"CDLA-Permissive-2.0",
|
||||
"GPL-3.0-or-later",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-3.0",
|
||||
"Unlicense",
|
||||
"Zlib",
|
||||
]
|
||||
confidence-threshold = 0.9
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn"
|
||||
wildcards = "deny"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
@@ -0,0 +1,187 @@
|
||||
# Compatibility specification
|
||||
|
||||
This is the on-disk and on-wire contract between the Go implementation
|
||||
(frozen in `oracle/go`, commit `4c1507c`) and the Rust implementation. It is
|
||||
derived from the Go sources and pinned by vectors the Go oracle generated
|
||||
(`tests/vectors`, regenerated with `oracle/gen-vectors.sh`). The Rust port is
|
||||
an implementation migration, not a format migration: nothing here changes,
|
||||
no data is rekeyed or converted, and no new suite becomes the default.
|
||||
|
||||
Each section names the tests that hold the Rust code to it. "Go" below always
|
||||
means the oracle.
|
||||
|
||||
## 1. Key file
|
||||
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| Encoding | one PEM block, type `VAULT KEY`, standard base64 with padding, 64 columns, as Go's `pem.Encode` |
|
||||
| Body | exactly 198 bytes: an 18-byte header and two 90-byte slots |
|
||||
| Header | `VAULTKEY1`, Argon2id time `3` (u32 BE), memory `65536` KiB (u32 BE), lanes `4` (u8) |
|
||||
| Slot | 16-byte salt, 24-byte XChaCha20-Poly1305 nonce, 34 bytes sealed (u16 BE suite ID, 32-byte master seed), 16-byte tag |
|
||||
| Slot key | Argon2id v1.3 (passphrase bytes as given, no normalisation, salt, t=3, m=64 MiB, p=4, 32 bytes) |
|
||||
| Associated data | the 18-byte header |
|
||||
| Second slot | a decoy identity's slot, or 90 random bytes; which slot is real is random |
|
||||
| Unlock | Argon2id runs for both slots every time (Rust runs them concurrently); the first slot that opens wins; "incorrect passphrase" if neither |
|
||||
|
||||
Accepted framing: optional text before the block (Go's `pem.Decode`), only
|
||||
whitespace after it, header lines inside the block ignored. Files over 64 KiB
|
||||
are refused outright (Go would read them, then reject them).
|
||||
|
||||
Tests: `keyfile::tests::*` (Argon2id vectors, Go-written key files with and
|
||||
without decoy open to the Go-recorded seeds, byte-identical PEM re-encoding,
|
||||
malformed framing, NFC/NFD passphrases differ).
|
||||
|
||||
## 2. Hybrid suite `0x0001`, tag `frodo640shake+mlkem768`
|
||||
|
||||
- Master seed: 32 bytes. Expansion:
|
||||
`HKDF-SHA-256(ikm = seed, salt = none, info = "fury/v1 seed-expand frodo640shake+mlkem768")`,
|
||||
read as 48 bytes of FrodoKEM seed then 64 bytes of ML-KEM seed.
|
||||
- FrodoKEM-640-SHAKE exactly as CIRCL v1.6.4 `kem/frodo/frodo640shake`
|
||||
(the round-3 FrodoKEM, not the ISO/eFrodoKEM variants): key seed
|
||||
`s(16) || seedSE(16) || z(16)`; public key 9616 bytes (`seedA || pack(B)`);
|
||||
private key 19888 bytes (`s || pk || S as u16 LE || H(pk)`); ciphertext 9720
|
||||
bytes; 16-byte shared secret; 16-byte encapsulation seed `mu`; implicit
|
||||
rejection returns `SHAKE128(ct || s)`.
|
||||
- ML-KEM-768 (FIPS 203) with seed `d || z`; encapsulation key 1184 bytes,
|
||||
expanded decapsulation key 2400 bytes, ciphertext 1088 bytes, 32-byte
|
||||
shared secret, implicit rejection.
|
||||
- Public key: FrodoKEM public key `||` ML-KEM encapsulation key (10800 bytes).
|
||||
Private key (for comparison only): FrodoKEM private `||` ML-KEM expanded.
|
||||
- Wrap: fresh, independent randomness for both encapsulations every time.
|
||||
`KEK = HKDF-SHA-256(ikm = ss1 || ss2, salt = none, info = "fury/v1 kek frodo640shake+mlkem768" || ct1 || ct2)`;
|
||||
the 32-byte file key is sealed with ChaCha20-Poly1305 under the KEK and an
|
||||
all-zero 12-byte nonce. The zero nonce is safe only because each KEK is new
|
||||
and used once; the Rust code keeps that sealing function private to the
|
||||
suite.
|
||||
- Stanza: type `frodo640shake+mlkem768`, two arguments
|
||||
`base64(ct1)`, `base64(ct2)`, body = 48-byte sealed file key.
|
||||
- Unwrap: a stanza of another type is "not mine"; wrong argument count,
|
||||
bad base64 or wrong ciphertext lengths are fatal for the file; an AEAD
|
||||
failure (wrong key) is "not mine". The first stanza that opens wins.
|
||||
|
||||
Rust implementation: FrodoKEM is a line-by-line port of CIRCL
|
||||
(`src/crypto/frodo.rs`) because no maintained Rust crate implements CIRCL's
|
||||
variant (RustCrypto `frodo-kem` 0.1 implements the salted ISO version with
|
||||
9752-byte ciphertexts). ML-KEM is RustCrypto `ml-kem` 0.3.2.
|
||||
|
||||
Tests: `crypto::frodo::tests::nist_kat_digest` (reproduces the SHA-256
|
||||
CIRCL checks against the FrodoKEM reference KAT file),
|
||||
`crypto::kem::tests::{frodo_matches_circl, mlkem_matches_circl,
|
||||
hybrid_derivation_matches_go, wrap_matches_go_and_unwraps,
|
||||
fresh_wraps_differ_and_foreign_keys_do_not_match}`.
|
||||
|
||||
## 3. Fury header
|
||||
|
||||
```text
|
||||
fury-encryption.org/v1\n
|
||||
-> TYPE ARG ARG\n per stanza; TYPE and ARGs are 1+ bytes of 0x21-0x7e
|
||||
BODY, 64 base64 columns per line, ending with a line shorter than 64
|
||||
--- MAC\n MAC = base64 of 32 bytes
|
||||
```
|
||||
|
||||
- Base64: standard alphabet, no padding, non-zero trailing bits rejected,
|
||||
CR or LF anywhere in an encoded value rejected.
|
||||
- A body whose encoding fills its last line (the 48-byte wrapped key encodes
|
||||
to exactly 64 characters) is followed by an empty line; serialisation is
|
||||
canonical and Rust reproduces Go's bytes exactly.
|
||||
- Limits: 1 MiB per stanza (all its lines), 16 MiB per header; a line must
|
||||
end in LF; three bytes of look-ahead decide between a stanza and the
|
||||
footer; the payload starts immediately after the footer's LF, with any
|
||||
read-ahead returned first.
|
||||
- MAC: `HMAC-SHA-256(key = HKDF-SHA-256(file key, salt none, info "header"), message = header up to and including "---")`,
|
||||
compared in constant time.
|
||||
- Payload: 16-byte nonce, then STREAM keyed by
|
||||
`HKDF-SHA-256(file key, salt = nonce, info "payload")`.
|
||||
|
||||
Tests: `crypto::fury::tests::{header_vectors_match_go,
|
||||
header_parser_agrees_with_go, decrypts_go_files}` (the parser's accept or
|
||||
reject verdict, and the payload bytes left after the header, match Go on
|
||||
every case in `crypto.json`), `crypto::header::tests::*`,
|
||||
`tests/interop_crypto.rs` (Go decrypts files Rust wrote, and rejects
|
||||
truncated, extended and corrupted ones).
|
||||
|
||||
## 4. STREAM
|
||||
|
||||
- 64 KiB plaintext chunks; ChaCha20-Poly1305; 16-byte tag per chunk.
|
||||
- Nonce: 12 bytes, bytes 3..11 the big-endian chunk index, last byte `1`
|
||||
on the final chunk only, `0` otherwise.
|
||||
- Only the final chunk may be short; it may be empty only when it is the
|
||||
only chunk (empty plaintext). A plaintext that is an exact multiple of
|
||||
64 KiB ends with a full-length final chunk.
|
||||
- Rejected: no final chunk (truncation), an empty final chunk after
|
||||
others, bytes after the final chunk, any chunk failing authentication,
|
||||
a payload length no STREAM encoding can have.
|
||||
- The index never wraps; Rust refuses to go past 2^64 − 1 chunks (Go's
|
||||
counter is 88 bits; neither limit is reachable in practice).
|
||||
- Random access (`StreamReaderAt`) authenticates the final chunk when
|
||||
constructed, then any chunk on demand.
|
||||
|
||||
Tests: `crypto::stream::tests::*` (Go vectors for sizes around every
|
||||
boundary, any write/read step sizes, malformations rejected identically by
|
||||
the streaming, in-memory and random-access readers, concurrency of the
|
||||
random-access reader, counter overflow).
|
||||
|
||||
## 5. Store
|
||||
|
||||
- `DIR/.vault` contains exactly `VAULT 2\n`.
|
||||
- `DIR/records/xx/ID` and `DIR/objects/xx/ID`: `ID` is 16 bytes as
|
||||
lowercase hex, `xx` its first byte; all 256 shards of both exist after
|
||||
`init`. Entries whose names are not exactly that form, or are in the
|
||||
wrong shard, are ignored.
|
||||
- Every ciphertext, every shard and both top directories have access and
|
||||
modification times pinned to the Unix epoch; removals re-pin the shard.
|
||||
- Record: a Fury file whose plaintext is exactly 4096 bytes: a u32 BE JSON
|
||||
length (≤ 4092), the JSON, random padding. JSON is Go's `encoding/json`
|
||||
output for `{"name","seed","size","mode","modified","added"}`: HTML
|
||||
characters and U+2028/2029 escaped, times RFC 3339 with up to nine
|
||||
fractional digits, trailing zeros removed, `Z`. Rust writes it
|
||||
byte-identically.
|
||||
- Validation on read: name valid (below), seed 64 lowercase hex characters,
|
||||
size 0..=1 TiB, both times valid RFC 3339; unknown fields refused.
|
||||
- Names: non-empty UTF-8, ≤ 1024 bytes, already clean (Go `path.Clean`),
|
||||
relative, not `.`/`..`/`../…`, no backslash, no control characters.
|
||||
- Object IDs: `HMAC-SHA-256(key = seed, msg = index as big-endian i64)[:16]`.
|
||||
- Objects: plaintext = own ID (16 bytes), then up to `4 MiB − 16` file
|
||||
bytes, then random padding to the next power of two from 64 KiB to 4 MiB.
|
||||
An empty file still has one object. These sizes are plaintext; the
|
||||
ciphertext adds the header (~14.6 KB) and 16 bytes per 64 KiB.
|
||||
- Reading an object checks: it opens, its first 16 bytes are its ID, its
|
||||
length is exactly the padded size for its part, and the whole STREAM
|
||||
(final chunk, end of file) authenticates.
|
||||
- Records the key cannot open (no stanza matches) are skipped silently;
|
||||
ones that open but are malformed are reported and skipped.
|
||||
- Duplicates (an interrupted replace) are ordered newest `added` first; the
|
||||
next write of that name removes them all.
|
||||
- Filler: `init` writes 8–32 entries (or more, to reach `--filler`) of
|
||||
log-uniform sizes from 1 KiB to `clamp(target/8, 4 MiB, 1 GiB)`, sealed to
|
||||
a throwaway key, through the same code as `add`.
|
||||
|
||||
Tests: `store::types::tests::*` (names, object IDs, padded sizes, object
|
||||
counts, record JSON byte-identical to Go), `store::tests::reads_go_vault`
|
||||
(the oracle's fixture vault: real files, decoy file, foreign record),
|
||||
`store::tests::*`, `tests/cli.rs` lifecycle tests in both directions.
|
||||
|
||||
## 6. Locks
|
||||
|
||||
`DIR/.lock`, created mode 0600. Unix: POSIX `fcntl` record locks over the
|
||||
whole file (`F_SETLK` then `F_SETLKW`), shared for readers, exclusive for
|
||||
writers, released by the kernel when the process dies. Windows:
|
||||
`LockFileEx` on byte 0, length 1. Other systems: an `O_EXCL` lock file.
|
||||
Tests: `tests/cli.rs::lock_*` (Rust waits for Go, Go waits for Rust, both
|
||||
proceed after the holder is killed).
|
||||
|
||||
## 7. Agent
|
||||
|
||||
Socket `$XDG_RUNTIME_DIR` (else `$TMPDIR`, else `/tmp`) `/vault-UID/agent-`
|
||||
followed by the first 8 bytes of SHA-256 of the key file's absolute path
|
||||
(Go `filepath.Abs`, including its use of `$PWD`) in hex. The directory must
|
||||
be the user's own with mode exactly 0700. One request per connection: `g`
|
||||
→ 34 bytes (u16 BE suite ID, seed); `q` → the agent wipes the key and
|
||||
closes. Two-second deadline per request. Tests:
|
||||
`tests/cli.rs::agent_*` (Rust client with Go agent and the reverse).
|
||||
|
||||
## 8. What is deliberately not identical
|
||||
|
||||
See `docs/MIGRATION.md` for the full list of intentional behaviour changes.
|
||||
None of them alters a byte that is written to the vault or the key file
|
||||
beyond what the Go format already allows (fresh randomness, timestamps).
|
||||
Generated
+1283
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
[package]
|
||||
name = "vault-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2024"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
tar = { version = "0.4.46", default-features = false }
|
||||
vault = { path = ".." }
|
||||
|
||||
# Not part of the main package's workspace.
|
||||
[workspace]
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[[bin]]
|
||||
name = "header"
|
||||
path = "fuzz_targets/header.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "stream"
|
||||
path = "fuzz_targets/stream.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "keyfile"
|
||||
path = "fuzz_targets/keyfile.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "record"
|
||||
path = "fuzz_targets/record.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "names"
|
||||
path = "fuzz_targets/names.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "html"
|
||||
path = "fuzz_targets/html.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "tar"
|
||||
path = "fuzz_targets/tar.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! The Fury header parser never panics, and accepts only canonical headers:
|
||||
//! whatever it accepts re-serialises to exactly the bytes it consumed.
|
||||
#![no_main]
|
||||
use std::io::Read;
|
||||
use vault::crypto::header;
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
if let Ok((h, mut rest)) = header::parse(data) {
|
||||
let mut tail = Vec::new();
|
||||
rest.read_to_end(&mut tail).unwrap();
|
||||
let consumed = &data[..data.len() - tail.len()];
|
||||
assert_eq!(h.marshal(), consumed, "non-canonical header accepted");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! The mirror's HTML and CSS rewriters never panic, and rewriting nothing
|
||||
//! returns the input byte for byte.
|
||||
#![no_main]
|
||||
use vault::net::html;
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let _ = html::tokenize(data);
|
||||
assert_eq!(html::rewrite_html(data, &mut |_| None, &mut |_| None), data);
|
||||
assert_eq!(html::rewrite_css(data, false, &mut |_| None), data);
|
||||
let _ = html::rewrite_html(data, &mut |_| None, &mut |u| Some(format!("x/{}", u.len())));
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
let _ = vault::net::Url::parse(s).map(|u| u.join("../a?b#c"));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! PEM decoding never panics, and decoding what it encoded is the identity.
|
||||
#![no_main]
|
||||
use vault::keyfile::{decode_pem, encode_pem, parse_body};
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
if let Some(body) = decode_pem(data) {
|
||||
let _ = parse_body(&body);
|
||||
assert_eq!(decode_pem(encode_pem(&body).as_bytes()), Some(body));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! Path validation: a valid vault name is clean, relative and confined; the
|
||||
//! tar name check only ever yields valid-shaped relative names.
|
||||
#![no_main]
|
||||
use vault::store::VaultName;
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let Ok(s) = std::str::from_utf8(data) else { return };
|
||||
if let Some(n) = VaultName::new(s) {
|
||||
let n = n.as_str();
|
||||
assert_eq!(vault::util::clean(n), n);
|
||||
assert!(!n.starts_with('/') && !n.contains('\\') && !n.split('/').any(|c| c == ".." || c == "." || c.is_empty()));
|
||||
assert!(n.len() <= 1024);
|
||||
}
|
||||
if let Some(n) = vault::absorb::safe_name(s) {
|
||||
assert!(!n.is_empty() && !n.starts_with('/') && !n.split('/').any(|c| c == ".."));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! Record JSON parsing never panics; an accepted record re-encodes (Go's
|
||||
//! JSON) to something that parses back to the same values.
|
||||
#![no_main]
|
||||
use vault::store::{Id, Record};
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(r) = Record::from_json(data, Id([0; 16])) {
|
||||
let Some(j) = r.to_json() else { return };
|
||||
let r2 = Record::from_json(&j, Id([0; 16])).expect("own encoding parses");
|
||||
assert_eq!(r.name, r2.name);
|
||||
assert_eq!((r.size, r.mode, r.modified, r.added), (r2.size, r2.mode, r2.modified, r2.added));
|
||||
assert!(r.seed.same_as(&r2.seed));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! The streaming, in-memory and random-access STREAM readers agree on every
|
||||
//! input: the same plaintext or all reject. Inputs are mostly forgeries;
|
||||
//! the first byte selects a mode that starts from a valid stream instead.
|
||||
#![no_main]
|
||||
use std::io::{Read, Write};
|
||||
use vault::crypto::stream::{StreamReader, StreamReaderAt, StreamWriter, open_all};
|
||||
|
||||
const KEY: [u8; 32] = [7; 32];
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let Some((&mode, rest)) = data.split_first() else { return };
|
||||
let input = if mode & 1 == 1 {
|
||||
// A valid stream of the given plaintext, then a mutation.
|
||||
let mut w = StreamWriter::new(&KEY, Vec::new());
|
||||
let n = rest.len().min(200_000);
|
||||
w.write_all(&rest[..n]).unwrap();
|
||||
let mut c = w.finish().unwrap();
|
||||
if mode & 2 == 2 && !c.is_empty() {
|
||||
let i = (mode as usize * 7919) % c.len();
|
||||
c[i] ^= mode | 1;
|
||||
}
|
||||
if mode & 4 == 4 {
|
||||
c.truncate(c.len().saturating_sub(mode as usize % 40));
|
||||
}
|
||||
c
|
||||
} else {
|
||||
rest.to_vec()
|
||||
};
|
||||
let mut a = Vec::new();
|
||||
let streaming = StreamReader::new(&KEY, &input[..]).read_to_end(&mut a).map(|_| a);
|
||||
let mut b = input.clone();
|
||||
let whole = open_all(&KEY, &mut b).map(|n| b[..n].to_vec());
|
||||
assert_eq!(streaming.is_ok(), whole.is_ok());
|
||||
if let (Ok(x), Ok(y)) = (&streaming, &whole) {
|
||||
assert_eq!(x, y);
|
||||
}
|
||||
if let Ok(r) = StreamReaderAt::new(&KEY, &input[..], input.len() as u64) {
|
||||
let mut out = vec![0u8; r.size() as usize];
|
||||
let got = r.read_at(&mut out, 0).map(|n| out[..n].to_vec());
|
||||
assert_eq!(got.is_ok(), whole.is_ok());
|
||||
if let (Ok(x), Ok(y)) = (got, whole) {
|
||||
assert_eq!(x, y);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! Walking arbitrary tar input as absorb does never panics, and every name
|
||||
//! it would store is confined.
|
||||
#![no_main]
|
||||
use std::io::Read;
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let mut a = tar::Archive::new(data);
|
||||
let Ok(entries) = a.entries() else { return };
|
||||
for e in entries.take(64) {
|
||||
let Ok(mut e) = e else { return };
|
||||
let name = e.path_bytes().into_owned();
|
||||
if let Some(n) = std::str::from_utf8(&name).ok().and_then(vault::absorb::safe_name) {
|
||||
assert!(!n.split('/').any(|c| c == ".."));
|
||||
}
|
||||
let mut sink = Vec::new();
|
||||
let _ = e.by_ref().take(1 << 20).read_to_end(&mut sink);
|
||||
}
|
||||
});
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the dixon-vault .deb, .rpm and Arch packages locally and show what
|
||||
# is in them. Nothing is signed or published: packaging/repo.sh does that,
|
||||
# reusing this script's build step.
|
||||
#
|
||||
# packaging/package.sh # from the working tree, into dist/packages
|
||||
# packaging/package.sh v0.6 # from a tag (a clean export of it)
|
||||
# OUT=/tmp/pkgs ARCHS="amd64 arm64" packaging/package.sh
|
||||
#
|
||||
# Needs: the pinned Rust toolchain with cargo-zigbuild and zig (the static
|
||||
# Linux binaries are cross-built), and go, only to run the pinned nfpm
|
||||
# packager. dpkg-deb, rpm and zstd are used for inspection when present.
|
||||
# None of these is needed to run vault itself.
|
||||
#
|
||||
# ARCHS package architectures (default below)
|
||||
# RPM_SIGNING_KEY armoured private key file to sign the .rpm with
|
||||
# (repo.sh sets it; unset here means unsigned)
|
||||
set -euo pipefail
|
||||
|
||||
# ---- project ---------------------------------------------------------------
|
||||
NAME=vault # the command
|
||||
# The package: plain "vault" is HashiCorp Vault in Arch's [extra] and in
|
||||
# HashiCorp's own apt and dnf repositories, which would win upgrades. Both
|
||||
# install /usr/bin/vault, so they conflict.
|
||||
PACKAGE=dixon-vault
|
||||
CONFLICTS=vault
|
||||
SUMMARY="Post-quantum encrypted file vault with a decoy passphrase"
|
||||
HOMEPAGE=https://dixon.cx/vault
|
||||
LICENSE=GPL-3.0-or-later
|
||||
MANPAGES=docs/*.1
|
||||
DOCS="README.md LICENSE SECURITY.md"
|
||||
# Package architecture -> Rust target (all static musl binaries).
|
||||
triple() {
|
||||
case $1 in
|
||||
amd64) echo x86_64-unknown-linux-musl ;;
|
||||
arm64) echo aarch64-unknown-linux-musl ;;
|
||||
arm) echo armv7-unknown-linux-musleabihf ;;
|
||||
386) echo i686-unknown-linux-musl ;;
|
||||
riscv64) echo riscv64gc-unknown-linux-musl ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
ARCHS=${ARCHS:-"amd64 arm64 arm 386 riscv64"}
|
||||
NFPM="go run github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.41.1"
|
||||
|
||||
say() { echo "package: $*" >&2; }
|
||||
die() { say "$*"; exit 1; }
|
||||
|
||||
# build_packages SRC OUTDIR VERSION: binaries, then one .deb, .rpm and
|
||||
# .pkg.tar.zst per architecture, into OUTDIR.
|
||||
build_packages() {
|
||||
local src=$1 outdir=$2 version=$3 arch nfarch t work
|
||||
for tool in cargo cargo-zigbuild zig go; do
|
||||
command -v $tool >/dev/null || die "needs $tool"
|
||||
done
|
||||
work=$(mktemp -d)
|
||||
mkdir -p "$outdir"
|
||||
for arch in $ARCHS; do
|
||||
t=$(triple "$arch") || die "no Rust target for $arch"
|
||||
nfarch=$arch
|
||||
[[ $arch == arm ]] && nfarch=arm7
|
||||
say "building $NAME for $arch ($t)"
|
||||
flags="--remap-path-prefix=$src=vault"
|
||||
# zig's musl getrandom() never reaches the kernel on riscv64; have
|
||||
# the getrandom crate issue the system call itself.
|
||||
[[ $t == riscv64*-musl ]] && flags+=' --cfg getrandom_backend="linux_raw"'
|
||||
(cd "$src" && RUSTFLAGS=$flags cargo zigbuild --quiet --release --locked --target "$t") || die "build for $arch failed"
|
||||
install -D -m 0755 "$src/target/$t/release/$NAME" "$work/bin-$arch/$NAME"
|
||||
{
|
||||
cat <<-EOF
|
||||
name: $PACKAGE
|
||||
arch: $nfarch
|
||||
platform: linux
|
||||
version: $version
|
||||
release: 1
|
||||
maintainer: Andy Dixon <andy@dixon.cx>
|
||||
description: $SUMMARY
|
||||
homepage: $HOMEPAGE
|
||||
license: $LICENSE
|
||||
conflicts: [${CONFLICTS:-}]
|
||||
contents:
|
||||
- src: $work/bin-$arch/$NAME
|
||||
dst: /usr/bin/$NAME
|
||||
file_info: {mode: 0755}
|
||||
EOF
|
||||
for m in $(cd "$src" && ls $MANPAGES); do
|
||||
printf ' - src: %s\n dst: /usr/share/man/man1/%s\n file_info: {mode: 0644}\n' \
|
||||
"$src/$m" "$(basename "$m")"
|
||||
done
|
||||
for d in $DOCS; do
|
||||
printf ' - src: %s\n dst: /usr/share/doc/%s/%s\n file_info: {mode: 0644}\n' \
|
||||
"$src/$d" "$PACKAGE" "$d"
|
||||
done
|
||||
if [[ -n ${RPM_SIGNING_KEY:-} ]]; then
|
||||
printf 'rpm:\n signature:\n key_file: %s\n' "$RPM_SIGNING_KEY"
|
||||
fi
|
||||
} >"$work/nfpm-$arch.yaml"
|
||||
for fmt in deb rpm archlinux; do
|
||||
$NFPM pkg -f "$work/nfpm-$arch.yaml" -p $fmt -t "$outdir/" >/dev/null
|
||||
done
|
||||
done
|
||||
rm -rf -- "$work"
|
||||
}
|
||||
|
||||
# inspect_packages DIR: list each package's metadata and files.
|
||||
inspect_packages() {
|
||||
local p
|
||||
for p in "$1"/*; do
|
||||
echo "== $(basename "$p")"
|
||||
case $p in
|
||||
*.deb) if command -v dpkg-deb >/dev/null; then dpkg-deb -f "$p" Package Version Architecture Conflicts; dpkg-deb -c "$p"; fi ;;
|
||||
*.rpm) if command -v rpm >/dev/null; then rpm -qpi "$p" 2>/dev/null | head -12; rpm -qplv "$p"; fi ;;
|
||||
*.pkg.tar.zst) if command -v zstd >/dev/null; then zstd -dcq "$p" | tar -tvf -; fi ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# When run (not sourced by repo.sh): build from the tree or a tag, inspect.
|
||||
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
out=${OUT:-dist/packages}
|
||||
mkdir -p -- "$out"
|
||||
out=$(cd -- "$out" && pwd -P)
|
||||
rm -f -- "$out"/*.deb "$out"/*.rpm "$out"/*.pkg.tar.zst
|
||||
if [[ $# -gt 0 ]]; then
|
||||
git rev-parse -q --verify "refs/tags/$1" >/dev/null || die "no tag $1"
|
||||
src=$(mktemp -d)
|
||||
trap 'rm -rf -- "$src"' EXIT
|
||||
git archive "$1" | tar -x -C "$src"
|
||||
version=${1#v}
|
||||
else
|
||||
src=$PWD
|
||||
version=$(cargo metadata --no-deps --format-version 1 | python3 -c 'import sys,json; print(json.load(sys.stdin)["packages"][0]["version"])')
|
||||
fi
|
||||
build_packages "$src" "$out" "$version"
|
||||
inspect_packages "$out"
|
||||
say "$(ls "$out" | wc -l) packages in $out (unsigned, not published)"
|
||||
fi
|
||||
+15
-61
@@ -10,37 +10,25 @@
|
||||
# Every index is rebuilt over all the packages in the repository, so other
|
||||
# projects publishing there with their copy of this script are kept.
|
||||
#
|
||||
# Needs: go, docker, gpg, apt-ftparchive (apt-utils), flock.
|
||||
# Needs: the pinned Rust toolchain with cargo-zigbuild and zig, go (to run
|
||||
# nfpm), docker, gpg, apt-ftparchive (apt-utils), flock.
|
||||
# REPO_DIR where the repository lives (default below)
|
||||
# REPO_GNUPGHOME GnuPG home holding the signing key (default below)
|
||||
# ARCHS Go architectures to package (default below)
|
||||
# ARCHS architectures to package (see packaging/package.sh)
|
||||
#
|
||||
# To build and inspect packages without signing or publishing, run
|
||||
# packaging/package.sh instead.
|
||||
#
|
||||
# The same script, with its own project block, is in ../mapsize/packaging.
|
||||
set -euo pipefail
|
||||
|
||||
# ---- project ---------------------------------------------------------------
|
||||
NAME=vault # the command
|
||||
# The package: plain "vault" is HashiCorp Vault in Arch's [extra] and in
|
||||
# HashiCorp's own apt and dnf repositories, which would win upgrades. Both
|
||||
# install /usr/bin/vault, so they conflict.
|
||||
PACKAGE=dixon-vault
|
||||
CONFLICTS=vault
|
||||
SUMMARY="Post-quantum encrypted file vault with a decoy passphrase"
|
||||
HOMEPAGE=https://dixon.cx/vault
|
||||
LICENSE=GPL-3.0-or-later
|
||||
MANPAGES=docs/*.1
|
||||
DOCS="README.md LICENSE"
|
||||
# build OUT: build the binary to OUT, in the source tree, with GOOS, GOARCH
|
||||
# and GOARM set and VERSION the release.
|
||||
build() {
|
||||
CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags='-s -w -buildid=' -o "$1" .
|
||||
}
|
||||
# ----------------------------------------------------------------------------
|
||||
# The project (package name, contents, architectures) and the build live in
|
||||
# packaging/package.sh, which can also be run on its own to build and inspect
|
||||
# packages without signing or publishing anything.
|
||||
. "$(dirname "$0")/package.sh"
|
||||
|
||||
REPO_DIR=${REPO_DIR:-/home/andy/domains/repo.dixon.cx/public_html}
|
||||
REPO_GNUPGHOME=${REPO_GNUPGHOME:-$HOME/.config/repo.dixon.cx/gnupg}
|
||||
ARCHS=${ARCHS:-"amd64 arm64 arm 386 riscv64"}
|
||||
NFPM="go run github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.41.1"
|
||||
SUITE=stable
|
||||
COMPONENT=main
|
||||
ARCH_REPO=dixon # the pacman repository name: [dixon]
|
||||
@@ -48,7 +36,7 @@ ARCH_REPO=dixon # the pacman repository name: [dixon]
|
||||
say() { echo "repo: $*" >&2; }
|
||||
die() { say "$*"; exit 1; }
|
||||
|
||||
for tool in go docker gpg apt-ftparchive flock; do
|
||||
for tool in cargo cargo-zigbuild zig go docker gpg apt-ftparchive flock; do
|
||||
command -v $tool >/dev/null || die "needs $tool"
|
||||
done
|
||||
[[ -d $REPO_DIR ]] || die "$REPO_DIR does not exist"
|
||||
@@ -64,47 +52,13 @@ export VERSION
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
git archive "$TAG" | tar -x -C "$WORK" --one-top-level=src
|
||||
mkdir -p "$WORK/src"
|
||||
git archive "$TAG" | tar -x -C "$WORK/src"
|
||||
gpg --batch --armor --export-secret-keys "$KEY" >"$WORK/signing.asc"
|
||||
|
||||
# ---- build -----------------------------------------------------------------
|
||||
# ---- build ----------------------------------------------------------------
|
||||
say "building $PACKAGE $VERSION for $ARCHS"
|
||||
mkdir -p "$WORK/out"
|
||||
for arch in $ARCHS; do
|
||||
goarm= nfarch=$arch
|
||||
[[ $arch == arm ]] && goarm=7 nfarch=arm7
|
||||
(cd "$WORK/src" && GOOS=linux GOARCH=$arch GOARM=$goarm build "$WORK/bin-$arch/$NAME")
|
||||
{
|
||||
cat <<-EOF
|
||||
name: $PACKAGE
|
||||
arch: $nfarch
|
||||
platform: linux
|
||||
version: $VERSION
|
||||
release: 1
|
||||
maintainer: Andy Dixon <andy@dixon.cx>
|
||||
description: $SUMMARY
|
||||
homepage: $HOMEPAGE
|
||||
license: $LICENSE
|
||||
conflicts: [${CONFLICTS:-}]
|
||||
contents:
|
||||
- src: $WORK/bin-$arch/$NAME
|
||||
dst: /usr/bin/$NAME
|
||||
file_info: {mode: 0755}
|
||||
EOF
|
||||
for m in $(cd "$WORK/src" && ls $MANPAGES); do
|
||||
printf ' - src: %s\n dst: /usr/share/man/man1/%s\n file_info: {mode: 0644}\n' \
|
||||
"$WORK/src/$m" "$(basename "$m")"
|
||||
done
|
||||
for d in $DOCS; do
|
||||
printf ' - src: %s\n dst: /usr/share/doc/%s/%s\n file_info: {mode: 0644}\n' \
|
||||
"$WORK/src/$d" "$PACKAGE" "$d"
|
||||
done
|
||||
printf 'rpm:\n signature:\n key_file: %s\n' "$WORK/signing.asc"
|
||||
} >"$WORK/nfpm-$arch.yaml"
|
||||
for fmt in deb rpm archlinux; do
|
||||
$NFPM pkg -f "$WORK/nfpm-$arch.yaml" -p $fmt -t "$WORK/out/" >/dev/null
|
||||
done
|
||||
done
|
||||
RPM_SIGNING_KEY=$WORK/signing.asc build_packages "$WORK/src" "$WORK/out" "$VERSION"
|
||||
|
||||
# ---- publish ---------------------------------------------------------------
|
||||
# One publisher at a time: the indexes cover every project's packages.
|
||||
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runtime smoke test of one vault binary, optionally under an emulator:
|
||||
# create a key and vault, store files across object boundaries, read them
|
||||
# back, then (if a reference binary is given) check the reference can read
|
||||
# what this binary wrote and the other way round.
|
||||
#
|
||||
# scripts/smoke.sh BINARY [REFERENCE_BINARY]
|
||||
# EMU="qemu-aarch64" scripts/smoke.sh dist/vault-linux-arm64 target/release/vault
|
||||
#
|
||||
# Uses a private temporary directory, removed afterwards. The passphrase is
|
||||
# a public test value.
|
||||
set -euo pipefail
|
||||
bin=$(realpath "$1")
|
||||
ref=${2:+$(realpath "$2")}
|
||||
emu=${EMU:-}
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/vault-smoke.XXXXXX")
|
||||
trap 'rm -rf -- "$work"' EXIT
|
||||
cd "$work"
|
||||
export VAULT_PASSPHRASE=smoke-test-passphrase VAULT_KEY=$work/key VAULT_DIR=$work/store
|
||||
export XDG_RUNTIME_DIR=$work/run
|
||||
mkdir -m 700 run
|
||||
v() { $emu "$bin" "$@"; }
|
||||
|
||||
v genkey 2>/dev/null
|
||||
v init 2>/dev/null
|
||||
head -c $((4 * 1024 * 1024 + 123)) /dev/urandom >big
|
||||
printf 'small\n' >small
|
||||
: >empty
|
||||
v add big 2>/dev/null
|
||||
v add small dir 2>/dev/null
|
||||
v add empty dir 2>/dev/null
|
||||
[[ $(v list | wc -l) == 3 ]] || { echo "FAIL list"; exit 1; }
|
||||
v get big -o big.out 2>/dev/null && cmp big big.out
|
||||
v get dir/small -o small.out 2>/dev/null && cmp small small.out
|
||||
if [[ -n $ref ]]; then
|
||||
"$ref" get big -o big.ref 2>/dev/null && cmp big big.ref
|
||||
printf 'from the reference\n' >r.txt
|
||||
"$ref" add r.txt 2>/dev/null
|
||||
v get r.txt -o r.out 2>/dev/null && cmp r.txt r.out
|
||||
fi
|
||||
v rm -rf dir 2>/dev/null
|
||||
[[ $(v list | wc -l) == $([[ -n $ref ]] && echo 2 || echo 1) ]] || { echo "FAIL rm"; exit 1; }
|
||||
echo "ok $(basename "$bin")${emu:+ (under $emu)}"
|
||||
+4
-3
@@ -90,9 +90,10 @@ fn mtime<R: Read>(entry: &mut tar::Entry<R>) -> Result<Instant> {
|
||||
if let Ok(Some(exts)) = entry.pax_extensions() {
|
||||
for ext in exts.flatten() {
|
||||
if ext.key_bytes() == b"mtime"
|
||||
&& let Some(t) = std::str::from_utf8(ext.value_bytes()).ok().and_then(parse_pax_time) {
|
||||
return Ok(t);
|
||||
}
|
||||
&& let Some(t) = std::str::from_utf8(ext.value_bytes()).ok().and_then(parse_pax_time)
|
||||
{
|
||||
return Ok(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
let secs = entry
|
||||
|
||||
+17
-7
@@ -24,12 +24,18 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
pub const AGENT_CMD: &str = "__agent";
|
||||
#[cfg_attr(not(unix), allow(dead_code))]
|
||||
const GET: u8 = b'g';
|
||||
#[cfg_attr(not(unix), allow(dead_code))]
|
||||
const QUIT: u8 = b'q';
|
||||
#[cfg_attr(not(unix), allow(dead_code))]
|
||||
const REPLY: usize = 2 + crate::crypto::kem::SEED_SIZE;
|
||||
#[cfg_attr(not(unix), allow(dead_code))]
|
||||
const TIMEOUT: Duration = Duration::from_secs(2);
|
||||
/// How long `unlock` waits for a starting agent to say it is ready.
|
||||
#[cfg_attr(not(unix), allow(dead_code))]
|
||||
const START_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
#[cfg_attr(not(unix), allow(dead_code))]
|
||||
const SECRET_ENVS: [&str; 2] = ["VAULT_PASSPHRASE", "VAULT_DECOY_PASSPHRASE"];
|
||||
|
||||
/// Go's `filepath.Abs`: lexical, with `os.Getwd`'s preference for `$PWD`.
|
||||
@@ -53,10 +59,12 @@ fn go_getwd() -> std::io::Result<PathBuf> {
|
||||
let dot = std::fs::metadata(".")?;
|
||||
if let Some(pwd) = std::env::var_os("PWD").map(PathBuf::from)
|
||||
&& pwd.is_absolute()
|
||||
&& let Ok(m) = std::fs::metadata(&pwd)
|
||||
&& m.dev() == dot.dev() && m.ino() == dot.ino() {
|
||||
return Ok(pwd);
|
||||
}
|
||||
&& let Ok(m) = std::fs::metadata(&pwd)
|
||||
&& m.dev() == dot.dev()
|
||||
&& m.ino() == dot.ino()
|
||||
{
|
||||
return Ok(pwd);
|
||||
}
|
||||
std::env::current_dir()
|
||||
}
|
||||
|
||||
@@ -265,9 +273,11 @@ mod imp {
|
||||
// Remove the socket only if it is still the one this agent made.
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
if let Ok(m) = std::fs::symlink_metadata(sock)
|
||||
&& m.ino() == ours.ino() && m.dev() == ours.dev() {
|
||||
let _ = std::fs::remove_file(sock);
|
||||
}
|
||||
&& m.ino() == ours.ino()
|
||||
&& m.dev() == ours.dev()
|
||||
{
|
||||
let _ = std::fs::remove_file(sock);
|
||||
}
|
||||
};
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
+8
-6
@@ -94,9 +94,10 @@ macro_rules! say {
|
||||
fn out(line: &str) {
|
||||
let mut o = std::io::stdout().lock();
|
||||
if let Err(e) = o.write_all(line.as_bytes()).and_then(|_| o.flush())
|
||||
&& e.kind() == std::io::ErrorKind::BrokenPipe {
|
||||
std::process::exit(128 + 13);
|
||||
}
|
||||
&& e.kind() == std::io::ErrorKind::BrokenPipe
|
||||
{
|
||||
std::process::exit(128 + 13);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -319,9 +320,10 @@ fn default_path(flag: &Option<String>, env: &str, name: &str) -> Result<PathBuf>
|
||||
/// environment, else from the key file with the passphrase.
|
||||
fn key_identity(key: &Path) -> Result<crate::crypto::kem::Identity> {
|
||||
if std::env::var_os(PASSPHRASE_ENV).is_none()
|
||||
&& let Ok(id) = agent::identity(key) {
|
||||
return Ok(id);
|
||||
}
|
||||
&& let Ok(id) = agent::identity(key)
|
||||
{
|
||||
return Ok(id);
|
||||
}
|
||||
let pass = read_passphrase("Enter passphrase:")?;
|
||||
keyfile::unlock(key, pass.expose()).map_err(|e| Error::new(e.to_string()))
|
||||
}
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@
|
||||
//! it; that file holds plaintext until it is removed, on every path, once
|
||||
//! stored. HTML and CSS for rewriting are held in memory instead.
|
||||
|
||||
mod html;
|
||||
mod url;
|
||||
pub mod html;
|
||||
pub mod url;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
+15
-1
@@ -81,7 +81,12 @@ fn unescape(s: &str, mode: Mode) -> Result<String, String> {
|
||||
b'%' => {
|
||||
let (h, l) = match (b.get(i + 1).copied().and_then(unhex), b.get(i + 2).copied().and_then(unhex)) {
|
||||
(Some(h), Some(l)) => (h, l),
|
||||
_ => return Err(format!("invalid URL escape {:?}", &s[i..(i + 3).min(s.len())])),
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"invalid URL escape {:?}",
|
||||
String::from_utf8_lossy(&b[i..(i + 3).min(b.len())])
|
||||
));
|
||||
}
|
||||
};
|
||||
if mode == Mode::Host && h < 8 && &s[i..i + 3] != "%25" {
|
||||
return Err(format!("invalid URL escape {:?}", &s[i..i + 3]));
|
||||
@@ -447,6 +452,15 @@ pub(crate) mod tests {
|
||||
assert_eq!(super::super::key(&u), s("key"), "{ctx} key");
|
||||
}
|
||||
|
||||
/// Found by fuzzing: a bad escape before a multi-byte character used to
|
||||
/// slice through the character and panic.
|
||||
#[test]
|
||||
fn bad_escape_before_multibyte_character() {
|
||||
assert!(Url::parse("/K%\u{8e3}").is_err());
|
||||
assert!(Url::parse("http://h/%é").is_err());
|
||||
assert!(Url::parse("http://h/%2").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_matches_go() {
|
||||
for v in vectors()["parse"].as_array().unwrap() {
|
||||
|
||||
@@ -36,6 +36,7 @@ pub fn create_private_dir_all(p: &Path) -> io::Result<()> {
|
||||
}
|
||||
|
||||
pub fn create_private_dir(p: &Path) -> io::Result<()> {
|
||||
#[cfg_attr(not(unix), allow(unused_mut))]
|
||||
let mut b = std::fs::DirBuilder::new();
|
||||
#[cfg(unix)]
|
||||
b.mode(0o700);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
//! `/proc/PID/mem` access. Other Unix systems get the rlimit only. Windows
|
||||
//! is not supported by the agent (see SECURITY.md).
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::io;
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
+6
-1
@@ -2,11 +2,15 @@
|
||||
|
||||
//! The real binaries, end to end: the Rust CLI on its own, and the Rust
|
||||
//! and Go CLIs sharing one key, one vault, one lock and one agent.
|
||||
// The lock and agent tests are Unix-only; their imports are unused elsewhere.
|
||||
#![cfg_attr(not(unix), allow(unused_imports))]
|
||||
|
||||
mod common;
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(unix)]
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Output, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -286,6 +290,7 @@ fn absorb_from_stdin() {
|
||||
/// Starts `absorb` and feeds it one byte: it takes the exclusive lock and
|
||||
/// then holds it until its standard input closes or it is killed. The
|
||||
/// caller kills and waits for it.
|
||||
#[cfg(unix)]
|
||||
#[allow(clippy::zombie_processes)]
|
||||
fn hold_lock(e: &Env, bin: &str) -> Child {
|
||||
let mut c = e
|
||||
|
||||
Reference in New Issue
Block a user