Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ vendor/

# goreleaser output
/dist/

# e2e run log (default E2E_LOG path)
/e2e/last-run.log
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ CAPS := cap_bpf,cap_perfmon,cap_sys_admin+ep
# Debian/Ubuntu keep the arch-specific <asm/*.h> uapi headers under a multiarch
# triplet (e.g. /usr/include/x86_64-linux-gnu); Arch keeps them in /usr/include.
# Add the triplet dir only when it exists, so the BPF object compiles on both.
# `?=` lets the environment override these (the Nix dev shell points them at the
# Nix-provided headers instead of /usr/include — see flake.nix).
ARCH_TRIPLET := $(shell uname -m)-linux-gnu
BPF_CFLAGS := -O2 -g -Wall -target bpf -I/usr/include \
BPF_CFLAGS ?= -O2 -g -Wall -target bpf -I/usr/include \
$(if $(wildcard /usr/include/$(ARCH_TRIPLET)/asm),-I/usr/include/$(ARCH_TRIPLET))

.PHONY: all build clean run setcap
.PHONY: all build clean run setcap e2e

# Default: build the binary, then grant it the eBPF caps so it runs unprivileged.
all: setcap
Expand Down Expand Up @@ -44,5 +46,12 @@ setcap: $(BIN)
run: build
sudo -E ./$(BIN) -verbose

# End-to-end harness: build + run the tool against the host's real YubiKey and
# exercise every supported tool. Runs inside the Nix dev shell (flake.nix) so
# the build toolchain and all the tools are provided. Needs a physical key + a
# human to touch it + sudo (eBPF caps). See e2e/README.md.
e2e:
nix develop --command ./e2e/run.sh

clean:
rm -f $(BPF_OBJ) $(BIN)
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ make # build, then grant caps (sudo) so it runs unprivileged
./whence-touche # or: make run (uses sudo)
```

Don't want to install `clang`/`libbpf`/Go yourself? `nix develop` drops you into
a shell with the whole build toolchain (see [`flake.nix`](flake.nix)), then
`make` works as above.

Or grab a prebuilt package for your distro from the
[latest release](https://github.com/Talgarr/Whence-Touche/releases), install it
(pick the line for your distro), and enable the user service:
Expand Down Expand Up @@ -79,6 +83,16 @@ Environment variables (prefix `WHENCE_`):

Unrecognised callers show the raw process chain.

## Testing

`go test ./...` covers the debounce/classification logic. For a real end-to-end
check, `make e2e` builds the tool and drives a live operation through **every
supported tool** against your actual YubiKey, asserting each is detected. It runs
in a [Nix dev shell](flake.nix) that provides the build toolchain plus all the
tools under test, so nothing needs installing. It needs a physical key and human
touches, so it's a local manual check rather than a CI job — see
[`e2e/`](e2e/README.md).

## License

MIT
73 changes: 73 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# End-to-end test

`run.sh` builds whence-touche and then drives a **real operation through every
supported tool**, asserting each one is detected and classified correctly.

It runs inside the **Nix dev shell** (`../flake.nix`), which provides both the
build toolchain (Go + the eBPF/clang toolchain) and every tool under test
(`gnupg`, `openssh`, `pass`, `gopass`, `sops`, `age`, `rage`, `git`, …) — so the
environment is reproducible and you don't have to install any of them yourself.

There is no GitHub workflow for this on purpose: a true e2e needs a physical
YubiKey and a human to touch it (and enter PINs), which CI can't provide.

## What it does

1. `make build` with the Nix toolchain (BPF object + binary).
2. `sudo setcap` the eBPF caps, then start the watcher with the **log-only**
notifier (`-notifier=log`) so detections land in a log it can grep.
3. For each tool: set up ephemeral artifacts using your YubiKey credential, run
the operation that needs a touch, and check the classifier named the tool.

## Usage

```sh
make e2e # build, then test every supported tool
./e2e/run.sh # same (re-execs itself into `nix develop`)
./e2e/run.sh gpg ssh # only the named tools
```

Before each tool it prompts `[Enter] run · [s] skip`. Perform the touch (and any
PIN entry) when prompted; the operation blocks until you do. At the end it prints
a PASS / FAIL / SKIP matrix and exits non-zero if anything failed.

### Tools and how each is exercised

| Tool | Operation driven | Credential needed |
|---|---|---|
| `gpg` | `gpg --sign` | GPG key on the YubiKey |
| `pass` | `pass show` (decrypt) | GPG key on the YubiKey |
| `gopass` | `gopass show` (decrypt) | GPG key on the YubiKey |
| `sops` | `sops --decrypt` (PGP) | GPG key on the YubiKey |
| `git` | `git commit -S` (signed) | GPG key on the YubiKey |
| `ssh` | `ssh-keygen -t ed25519-sk` | FIDO2 PIN set on the key |
| `age` | `age -d` via `age-plugin-yubikey` | PIV identity (best-effort) |
| browser | opens webauthn.io in your default browser | a passkey/WebAuthn credential |

The watcher is granted only the eBPF caps (`cap_bpf`, `cap_perfmon`,
`cap_sys_admin`). An agent-mediated touch (gpg, pass, sops, …) is attributed to
the real client rather than to scdaemon by the in-kernel request graph, so no
`/proc`-scanning capabilities are required.

Tools whose credential isn't present are **SKIP**ped with a reason.

> **Touch policy matters.** eBPF detection fires on a sustained touch-*wait*. A
> credential whose touch policy is **off** completes too quickly to register, so
> that test will FAIL even though the tool itself works. Enable the touch policy
> (GPG: `ykman openpgp keys set-touch`; PIV: per-slot touch; FIDO: a PIN /
> `verify-required`) for the keys you test.

## Environment

| var | default | meaning |
|---|---|---|
| `WHENCE_E2E_GPG_KEY` | first secret key | GPG key fingerprint to use |
| `E2E_TOUCH_TIMEOUT` | `60` | seconds to wait for each touch |
| `E2E_DEBUG` | `0` | `1` runs the watcher with `-verbose` and prints, per test, the full process call stack the classifier saw (plus how the gpg/ssh-agent client was resolved) — use it to explain a misclassification |

## Requirements

- Linux host with eBPF + BTF (`/sys/kernel/btf/vmlinux`).
- Nix with flakes enabled (the script passes the experimental flags itself).
- `sudo` (to grant the eBPF caps via `setcap`).
- A physical YubiKey configured for whatever tools you test.
Loading