- Rust 87.1%
- Shell 3.2%
- Python 1.9%
- C 1.9%
- Java 0.9%
- Other 4.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
release / build / aarch64-apple-darwin (push) Has been cancelled
release / build / x86_64-apple-darwin (push) Has been cancelled
release / build / aarch64-unknown-linux-musl (push) Has been cancelled
release / publish release (push) Has been cancelled
release / build / x86_64-unknown-linux-musl (push) Has been cancelled
release / build / x86_64-pc-windows-gnu (push) Has been cancelled
release / docker image (push) Has been cancelled
release / deb / rpm packages (push) Has been cancelled
release / update homebrew tap (push) Has been cancelled
|
||
| .cargo | ||
| .forgejo/workflows | ||
| crates | ||
| docs | ||
| examples | ||
| fuzz | ||
| man | ||
| packaging | ||
| scripts | ||
| sdk | ||
| supply-chain | ||
| verification | ||
| .gitignore | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CODE_OF_CONDUCT.md | ||
| COMPARISON.md | ||
| CONTRIBUTING.md | ||
| deny.toml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| GOVERNANCE.md | ||
| INSTALL.md | ||
| LICENSE-AGPL-3.0 | ||
| LICENSE-COMMERCIAL | ||
| NOTICE | ||
| README.md | ||
| README.pt-BR.md | ||
| ROADMAP.md | ||
| rust-toolchain.toml | ||
| SECURITY.md | ||
Evelin
Post-quantum secure tunnel. SSH-shaped toolchain, Rust, ML-KEM-1024 + ML-DSA-87 + ChaCha20-Poly1305.
Version: 4.3.0
License: AGPL-3.0-or-later or commercial (see NOTICE)
Status: self-released; production-validated against 1× internet-exposed Debian 12 host since v2.0
Author: Cristian Cezar Moisés / SecurityOps · sac@securityops.co
Project: GOVERNANCE.md · CONTRIBUTING.md · ROADMAP.md · CODE_OF_CONDUCT.md
Guides: getting started · architecture · extensions · examples · FAQ
Em português (secundário): README.pt-BR.md · docs/pt-br/
What's new in v4.3.0 (2026-07-24) — quiet-by-default client, scp-like copy
- The client is now quiet by default, like
ssh. A successful connection prints nothing but the remote session; the old{"timestamp":…,"message":"handshake complete",…}noise is gone. Use-v/-vv/-vvvfor connection detail (INFO/DEBUG/TRACE, likessh -v) or-qfor errors only.RUST_LOGstill overrides. - Logs never touch stdout anymore (text and json go to stderr), so
exec/pipeoutput andcpdata stay clean and pipeable. The client's default log format is now human-readabletext; the server daemon still defaults tojson. - scp-familiar
cp:-qsilences the progress bar/summary,-vadds per-file detail, and the bar now shows an explicit percentage ([■■■…] 42% · 1.26 MiB/3.00 MiB · 118 MiB/s · ETA 0s). Transfers stay byte-identical and BLAKE3-verified (… BLAKE3 verified · Quantum-secured!). - Client-side console/UX only — no wire, format, key, or ticket change; server untouched. 440 tests / 0 failures (run twice); clippy
-D warningsclean; verified live with the release binaries.
What's new in v4.2.0 (2026-07-20) — docs, perf, advisory bumps
- Documentation, EN + pt-BR: project governance set (
CONTRIBUTING.md,GOVERNANCE.md,ROADMAP.md,CODE_OF_CONDUCT.md) and five user guides underdocs/en/(getting started, architecture, extensions, examples, FAQ), each fact-checked against the source; full Brazilian-Portuguese mirror underdocs/pt-br/plusREADME.pt-BR.md. - Allocation/copy reduction on hot paths (honest scope: allocations and copies removed; no throughput claim — bulk remains core-contention-bound per
docs/BENCHMARKS.md): receive-side in-place decrypt (Aead::open_in_place, the recv mirror of v3.2'sseal_in_place, pinned by new equivalence tests); outbound Data frames pre-size the AEAD tag so seal no longer reallocates; mux dispatch releases the state lock before backpressured sends (head-of-line fix); filecopy sheds a per-chunk deep copy on all three bulk paths and re-reads nothing on resume (forked SHA-256 state). Wire bytes unchanged. - Dependency advisories cleared:
crossbeam-epoch0.9.20 (RUSTSEC-2026-0204; dev-dependency reach only) andanyhow1.0.104 (RUSTSEC-2026-0190);cargo auditgreen. - Test-suite robustness: a lost-exit-status race in the exec e2e fixed; hardcoded
/bin/echoremoved (Guix/NixOS hosts); debug-profile stack headroom for the big ML-KEM/ML-DSA handshake frames via.cargo/config.toml. - 439 tests / 0 failures (run twice); clippy
-D warningsclean; audit gate PASS. Source-level release — binary packages are not rebuilt by this commit.
What's new in v4.1.1 (2026-06-10) — docs + live validation
COMPARISON.md: honest Evelin↔OpenSSH comparison (verified OpenSSH 10.x PQ facts; losses stated plainly; no unbenchmarked performance claims).- Full-binary connection validation (
docs/V4_1_1_CONNECTION_VALIDATION.md): live v2 handshake with negotiated 64 KiB frames, 1 MiB filecopy roundtrip byte-identical, v1 fallback, auth negatives, handshake+exec median 11.7 ms (loopback, 1 core). Closes dev.3 R-1. - 44 superseded narrative .md files removed (manifest in CHANGELOG); zero dangling references. No code change.
What's new in v4.1.0 (2026-06-10) — deep-review release
- Operator-tunable frame size:
max_frame_kib(power of two in [16, 1024] KiB; default 1024 = byte-identical to v4.0.0) is advertised in the v2 offer; the negotiated min gates the record layer. Set below 1024 only when both peers are ≥ 4.1.0 (see ERRATA E-2). Honest note: this does not change measured single-core throughput (v3.7); its value is operational. - Strict v2 offer validation: a nonconforming offer (frame advertisement below the 16 KiB v1 base; malformed known-value shapes) now aborts the handshake coherently on both ends instead of being silently clamped. Intentional behaviour change, test-locked.
- Mux sized to the live frame budget: data fragments to the negotiated frame and oversized Requests fail their own call (
RequestTooLarge) instead of killing the connection — fixes v4.0.0 ERRATUM E-2. - Four v4.0.0 errata published and corrected (
docs/V4_0_0_ERRATA.md): an inaccurate test-coverage claim in the GA audit (E-1), the mux frame defect (E-2), a non-compiling session test suite in the shipped source (E-3), lenient capability-value parsing (E-4). - 233 tests / 0 failures across 11 crates (changed crates run twice); clippy clean; audit 0 vulns; deny PASS. Full review record incl. provenance:
docs/V4_1_AUDIT_REPORT.md.
What's new in v4.0.0 (2026-06-09) — GA
Protocol v2 reaches GA: opt-in, transcript-bound, downgrade-resistant capability negotiation. Default deployments stay byte-identical to v1.4 — v2 is strictly opt-in (set max_protocol_version = 2 on both ends).
- Downgrade resistance by construction: the capability block lives in the signed handshake body (never the header); stripping/altering/injecting it — or rewriting the version — breaks the ML-DSA-87 signature and aborts the handshake. Backed by executable tests and a real-TCP integration test.
- Negotiated record frame size (
MAX_FRAME_LOG2) clamps the record layer (proven: a 128 KiB frame roundtrips over a v2 socket, impossible under v1's 16 KiB cap).REKEYis negotiated/exposed but reserved for a future asymmetric ML-KEM rekey (not wired to the always-on ratchet — that would be ceremony). - Hardening:
Capabilities::decodegains ~570k-input property/fuzz coverage;SECURITY.mdgains a v2 threat-model section; Tamarin + ProVerif models of the shipped v2 negotiation are added STATED (not machine-checked — no prover binary here). - Honest limitations carried into GA: the v2 formal models are STATED not PROVEN; throughput is unchanged from v3.7 (AEAD ceiling measured, bulk is single-core-contention-bound) — the negotiated frame size changes what the record layer accepts, not the measured single-core throughput.
- Migration: no key/identity/ticket change; upgrade servers first (a v2 server still accepts v1 clients byte-identically), then clients. All gates green; full multi-platform packaging. See
docs/V4_0_0_AUDIT_REPORT.md.
Older release notes
The full per-release history (v0.1 → v4.0.0-dev.5) lives in CHANGELOG.md; per-release audit reports ship, checksummed, with each release's artifacts.
Earlier releases (v3.4 and before)
Condensed for brevity — see CHANGELOG.md for the full per-version history (v3.4 extension-negotiation claim correction, v3.3 crypto-claim correction, v3.2 allocation/copy reduction, v3.1 write-coalescing, v3.0 zeroization, and the v2.x security hardening arc).
What it is
Evelin is what SSH would be if you redesigned it in 2026 knowing classical cryptography has a clock on it. The handshake uses ML-KEM-1024 (FIPS 203, post-quantum key encapsulation) and ML-DSA-87 (FIPS 204, post-quantum signatures), both NIST security category 5. The record layer is ChaCha20-Poly1305 with HKDF-SHA-512 KDF. Identity keys can be passphrase-wrapped with Argon2id (m=64 MiB, t=3, p=1).
The user-facing toolchain shape is borrowed from OpenSSH on purpose, so operators don't need to relearn anything. The wire protocol is not SSH-compatible and never will be.
If you record traffic today and need it confidential ten or twenty years from now, classical SSH defaults are not enough. Evelin is for exactly that case.
What's in the toolchain
| Binary | OpenSSH analog | What it does |
|---|---|---|
evelin-server |
sshd |
server daemon |
evelin-client |
ssh |
client (exec, shell, file copy, port forward, SOCKS, jump, proxy-command) |
evelin-keygen |
ssh-keygen |
generate identity keys |
evelin-agent |
ssh-agent |
hold unlocked keys in memory |
evelin-keyscan |
ssh-keyscan |
discover server fingerprints |
evelin-multisig-verify |
— | verify release signatures |
evelin-sandbox-probe |
— | check landlock support on this host |
evelin-seccomp-probe |
— | check seccomp support on this host |
Server side — installation and first start
# 1. Install. Pick your platform:
sudo dpkg -i evelin_1.1.0_amd64.deb # Debian/Ubuntu
sudo rpm -i evelin-1.1.0-1.x86_64.rpm # Fedora/RHEL/Alma/Rocky
# OR drop the static tarball anywhere on $PATH:
tar xzf evelin-v1.1.0-x86_64-unknown-linux-musl.tar.gz
sudo install -m 0755 evelin-v1.1.0-*/bin/* /usr/local/bin/
# 2. Generate the server identity key.
sudo mkdir -p /etc/evelin
sudo evelin-keygen --out /etc/evelin/identity.key
sudo chmod 600 /etc/evelin/identity.key
sudo chown evelin:evelin /etc/evelin/identity.key 2>/dev/null || true
# 3. Set up the authorized_keys file (required header on first line).
sudo tee /etc/evelin/authorized_keys > /dev/null <<'EOF'
# evelin-authorized-keys v1
EOF
# 4. Set up the server config.
sudo tee /etc/evelin/server.toml > /dev/null <<EOF
bind = "0.0.0.0:7222"
identity_key = "/etc/evelin/identity.key"
authorized_keys = "/etc/evelin/authorized_keys"
log_format = "json"
log_level = "info"
[limits]
max_connections = 100
[exec]
allow = [] # exact-match command allow-list
[forward]
allow = []
wildcards = false
[reverse_forward]
allow = []
EOF
# 5. Sanity-check the config without binding the network:
sudo evelin-server --config /etc/evelin/server.toml --check
# 6. Note the server fingerprint — you'll need it on the client side.
sudo evelin-server --config /etc/evelin/server.toml --check | grep identity_key
# 7. Run as a systemd service (unit file is shipped in the .deb / .rpm):
sudo systemctl enable --now evelin-server
sudo systemctl status evelin-server
If you're not on a packaged distro, the systemd unit lives at packaging/systemd/evelin-server.service in the source tree.
Client side — first connection
# 1. Install (same package, both halves).
sudo dpkg -i evelin_1.1.0_amd64.deb # or
tar xzf evelin-v1.1.0-x86_64-unknown-linux-musl.tar.gz
sudo install -m 0755 evelin-v1.1.0-*/bin/* /usr/local/bin/
# 2. Generate your client identity key.
mkdir -p ~/.evelin
evelin-keygen --out ~/.evelin/identity.key
chmod 600 ~/.evelin/identity.key
# 3. Print the line to paste into the server's authorized_keys:
evelin-client --config ~/.evelin/client.toml print-auth-line --comment "$(whoami)@$(hostname)"
# → copy that hex fingerprint + comment into /etc/evelin/authorized_keys
# on the server (the line goes UNDER the v1 header).
# 4. Get the server's fingerprint out-of-band — phone, paper, signed
# email — and add it to your local trust file:
evelin-client --config ~/.evelin/client.toml \
trust --addr server.example.com:7222 \
--fingerprint <64-hex-from-server-out-of-band>
# 5. Connect:
cat > ~/.evelin/client.toml <<EOF
server_addr = "server.example.com:7222"
identity_key = "$HOME/.evelin/identity.key"
EOF
# Run a single command:
evelin-client --config ~/.evelin/client.toml exec uname -a
# Interactive shell:
evelin-client --config ~/.evelin/client.toml shell
# Copy a file:
evelin-client --config ~/.evelin/client.toml cp ./local.txt remote:/tmp/
# Local port forward (-L):
evelin-client --config ~/.evelin/client.toml forward -L 8080:internal.example:80
# Reverse port forward (-R):
evelin-client --config ~/.evelin/client.toml reverse-forward -R 9090:127.0.0.1:9090
# SOCKS5 dynamic forward (-D):
evelin-client --config ~/.evelin/client.toml socks -D 1080
# Through a jump host:
evelin-client --config ~/.evelin/client.toml --jump bastion.example:7222 exec hostname
# Through a corporate HTTP CONNECT proxy:
evelin-client --config ~/.evelin/client.toml \
--proxy-command 'corkscrew proxy.corp 8080 $EVELIN_HOST $EVELIN_PORT' \
exec hostname
Verifying signatures
Every release tarball ships with a SHA256SUMS file:
sha256sum -c SHA256SUMS
Releases produced by Forgejo CI are also PGP-signed by the project release key. The fingerprint is in SECURITY.md.
Build from source
git clone https://github.com/cristiancmoises/evelin
cd evelin
cargo build --release --workspace
ls target/release/evelin-*
Requirements: Rust 1.85+ (the rust-toolchain.toml pins this).
For static musl builds, see the cross-compile commands in BUILD.md.
Cryptography
| Layer | Algorithm | Source |
|---|---|---|
| Key encapsulation | ML-KEM-1024 (FIPS 203) | ml-kem 0.3 |
| Signatures | ML-DSA-87 (FIPS 204) | ml-dsa 0.1.0-rc.9 |
| AEAD | ChaCha20-Poly1305 | chacha20poly1305 0.10 |
| KDF | HKDF-SHA-512 | hkdf 0.12 + sha2 0.10 |
| Passphrase wrap | Argon2id (m=64 MiB, t=3, p=1) | argon2 0.5 |
| Hash | SHA-512, BLAKE2b | sha2, blake2 |
Identity-key fingerprints are SHA-512 truncated to 256 bits and rendered as 64 hex characters.
Deployment notes
These are real lessons from running v1.1.0 in production. Read this before your first install.
Use the musl static binary on any Linux VPS. The glibc binary (and the .deb / .rpm packages, which wrap the glibc binary) was built against glibc 2.39. It will not run on Debian 12 (glibc 2.36), Ubuntu 22.04 (glibc 2.35), or anything older. The musl static binary in evelin-v1.1.0-x86_64-unknown-linux-musl.tar.gz has zero glibc dependency and runs everywhere. Default to musl for VPSes; only use the glibc binary if you specifically know your host is recent enough.
[shell] requires the pty-shell Cargo feature at build time. The shipped musl tarball is built without it. If you want interactive PTY shell sessions (evelin-client shell), rebuild the server with cargo build --release --features pty-shell -p evelin-server (or use the binary published as evelin-server-v1.1.0-pty-shell-*). The config field [shell] enable = true is accepted by the parser regardless, but the server will refuse pty-req at runtime if the feature isn't compiled in.
Paths in client.toml and server.toml are not shell-expanded. TOML doesn't evaluate $HOME or ~. Use absolute paths only. Wrong: identity_key = "$HOME/.evelin/identity.key". Right: identity_key = "/home/cristian/.evelin/identity.key".
The [filecopy] schema is roots + allow_upload + allow_download. A single shared path-prefix list governs both directions. Read/write distinction is enforced by file mode and the user evelin-server runs as, not by separate config fields.
The [shell] schema is enable + command + args. The shell binary path is command, not program.
The systemd unit's user must own the key files. User=evelin in the unit means the evelin user must be able to read /etc/evelin/identity.key. Default packaging leaves the file root-owned, which crashes the service with a generic Permission denied. Fix:
sudo chown -R evelin:evelin /etc/evelin
sudo chmod 755 /etc/evelin
sudo chmod 600 /etc/evelin/identity.key
sudo chmod 644 /etc/evelin/authorized_keys /etc/evelin/server.toml
The systemd unit's ExecStart path must match where the binary actually is. If you installed via the musl tarball, the binary is at /usr/local/bin/evelin-server. The default unit file ships with /usr/sbin/evelin-server. Edit the unit (sudo systemctl edit --full evelin-server) so the paths match, or symlink. If you installed via .deb / .rpm and used the glibc binary, the path is /usr/bin/evelin-server.
The systemd unit's ReadWritePaths must include every path declared in [filecopy] roots. If roots = ["/srv/uploads/"], then ReadWritePaths=/srv/uploads. Otherwise the server's filesystem-write attempts get blocked by ProtectSystem=strict even though the user has the right permissions.
The client has no --log-level flag. Verbosity is controlled by log_level in client.toml, or by setting RUST_LOG=debug in the environment. Same applies to the server.
authorized_keys must start with the header # evelin-authorized-keys v1. A file without it is rejected at parse time with a confusing-looking line-number error. The print-auth-line subcommand emits a comment line above the fingerprint — paste only the fingerprint line, not the comment.
Status
v1.1.0 is self-released. The maintainer has deployed it on their own infrastructure and considers it ready for self-deployment by others. v1.1.0 does NOT reflect a paid third-party cryptographic audit; that's still a future event the project welcomes. See SECURITY.md for the full residual-risk disposition.
Project history
- 2026-05-08 — first end-to-end production use:
- First file transfer over the post-quantum tunnel:
love.txt → remote:/tmp/life.txt, laptop to VPS. - First interactive PTY shell session through Evelin, laptop into VPS shell.
- Build: v1.1.0 musl static +
pty-shellfeature, Debian 12 VPS, systemd-managed with full sandboxing (Landlock fully enforced).
- First file transfer over the post-quantum tunnel:
License
Dual-licensed:
- AGPL-3.0-or-later — free software, with strong copyleft for network use. Source:
LICENSE-AGPL-3.0. - Commercial license — for use that's incompatible with the AGPL (closed-source embedding, modified service without source disclosure, organizations that can't accept AGPL). Inquiries:
sac@securityops.co.
See NOTICE for the full plain-language explanation. The commercial license exists because the author wrote this code and wants to be paid when companies build businesses on top of it. Individual / hobbyist / research / non-profit / internal-corporate-use is fully covered by the AGPL — no payment required.
Reporting security issues
Don't open public issues for security vulnerabilities. See SECURITY.md for the encrypted-channel reporting flow.
Project facts
- 12 crates, 78 Rust files, ~16,400 lines of Rust
- Zero
unsafecode (workspace lint enforced) - Clippy
-D warningsclean - 134+ tests across the cryptographic and protocol crates
- 6 fuzz targets, 50.4M iterations cumulative, 0 crashes (carried over from v0.3.0)
- Formal models (primary handshake): 5 Tamarin lemmas + 8 ProVerif queries machine-checked in the v0.1 proof ceremony (core secrecy, mutual authentication, perfect forward secrecy). A further 3 Tamarin lemmas + 2 ProVerif queries (ratchet / agent-binding / filecopy-integrity) are stated but not yet machine-checked, and the v1.5/v1.8 extension models are stated + aspirational (they model features not wired into the shipping code). Full per-lemma status:
verification/VERIFICATION_STATUS.md. Not "fully formally verified" — see that file for the honest breakdown. - Reproducible builds verified across two independent hosts (carried over from v0.3.0)