Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .claude/skills/releng/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ If signing fails with `Inappropriate ioctl for device`, ask the user to unlock t

## 4. Release workflow

The tag triggers `.github/workflows/release.yml`: test -> create-release (DRAFT, checks the tag matches `Cargo.toml`) -> upload-binaries (linux musl x86_64/aarch64 with rustls, x86_64/aarch64 apple-darwin; each archive holds `mairu` and its attestation `mairu.sigstore.json`) and upload-debs (attested too) -> publish-crate (crates.io trusted publishing, runs last).
The tag triggers `.github/workflows/release.yml`: test -> create-release (DRAFT, checks the tag matches `Cargo.toml`) -> upload-binaries (linux musl x86_64/aarch64 with rustls, x86_64/aarch64 apple-darwin; each archive holds `mairu` and its attestation `mairu.sigstore.json`) and upload-debs (attested too), both after windows-hello (builds `mairu-windows-hello.exe` for x86_64/aarch64-pc-windows-msvc, attests it, uploads `mairu-windows-hello-<target>.zip`; the Linux archives and debs ship the matching `.exe` and record its SHA-256) -> publish-crate (crates.io trusted publishing, runs last).

Watch it (`gh run watch`). (user) The user publishes the draft release.

Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ jobs:
- run: cargo build --target ${{ matrix.target }} --no-default-features --features platform-helper --bin mairu-platform-helper
- run: cargo clippy --target ${{ matrix.target }} --no-default-features --features platform-helper --all-targets -- -D warnings

windows-hello-helper:
name: cargo build (Windows Hello helper)
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
target: [x86_64-pc-windows-msvc, aarch64-pc-windows-msvc]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
with: { target: "${{ matrix.target }}" }
- run: rustup component add clippy
- run: cargo build --target ${{ matrix.target }} --no-default-features --features windows-hello-helper --bin mairu-windows-hello
- run: cargo clippy --target ${{ matrix.target }} --no-default-features --features windows-hello-helper --all-targets -- -D warnings

linux-hardened:
name: linux-hardened
runs-on: ubuntu-latest
Expand Down
74 changes: 70 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,60 @@ jobs:
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'

upload-binaries:
# Linux archives and debs ship the helper of the matching Windows architecture, so it builds first.
windows-hello:
needs: create-release
strategy:
matrix:
target: [x86_64-pc-windows-msvc, aarch64-pc-windows-msvc]
runs-on: windows-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
with: { target: "${{ matrix.target }}" }
- run: "cargo build --release --target ${{ matrix.target }} --no-default-features --features windows-hello-helper --bin mairu-windows-hello"
- uses: actions/attest-build-provenance@v4
id: attest
with:
subject-path: "target/${{ matrix.target }}/release/mairu-windows-hello.exe"
- name: create archive
shell: bash
env:
TARGET: "${{ matrix.target }}"
BUNDLE_PATH: "${{ steps.attest.outputs.bundle-path }}"
run: |
mkdir -p dist
cp "target/${TARGET}/release/mairu-windows-hello.exe" dist/
cp "$BUNDLE_PATH" dist/mairu-windows-hello.exe.sigstore.json
cd dist
7z a "../mairu-windows-hello-${TARGET}.zip" mairu-windows-hello.exe mairu-windows-hello.exe.sigstore.json
- uses: actions/upload-artifact@v4
with:
name: "mairu-windows-hello-${{ matrix.target }}"
path: "target/${{ matrix.target }}/release/mairu-windows-hello.exe"
if-no-files-found: error
- run: 'gh release upload "${{ github.ref_name }}" "mairu-windows-hello-${{ matrix.target }}.zip"'
shell: bash
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'

upload-binaries:
needs: [create-release, windows-hello]
strategy:
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
features: rustls
windows-hello: x86_64-pc-windows-msvc
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
features: rustls
windows-hello: aarch64-pc-windows-msvc
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
Expand All @@ -71,6 +114,15 @@ jobs:
- uses: taiki-e/setup-cross-toolchain-action@v1
with:
target: "${{ matrix.target }}"
- uses: actions/download-artifact@v4
if: "${{ matrix.windows-hello }}"
with:
name: "mairu-windows-hello-${{ matrix.windows-hello }}"
path: target/windows-hello
- name: pin the Windows Hello helper
if: "${{ matrix.windows-hello }}"
shell: bash
run: echo "MAIRU_WINDOWS_HELLO_HELPER_SHA256=$(sha256sum target/windows-hello/mairu-windows-hello.exe | cut -d' ' -f1)" >> "$GITHUB_ENV"
- run: "cargo build --release --target ${{ matrix.target }} --no-default-features --features ${{ matrix.features || 'default' }}"
- uses: actions/attest-build-provenance@v4
id: attest
Expand All @@ -83,23 +135,30 @@ jobs:
BUNDLE_PATH: "${{ steps.attest.outputs.bundle-path }}"
run: |
mkdir -p dist
files=(mairu mairu.sigstore.json)
cp "target/${TARGET}/release/mairu" dist/
cp "$BUNDLE_PATH" dist/mairu.sigstore.json
tar -C dist -czf "mairu-${TARGET}.tar.gz" mairu mairu.sigstore.json
if [ -e target/windows-hello/mairu-windows-hello.exe ]; then
install -m 0755 target/windows-hello/mairu-windows-hello.exe dist/
files+=(mairu-windows-hello.exe)
fi
tar -C dist -czf "mairu-${TARGET}.tar.gz" "${files[@]}"
- run: 'gh release upload "${{ github.ref_name }}" "mairu-${{ matrix.target }}.tar.gz"'
shell: bash
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'

upload-debs:
needs: create-release
needs: [create-release, windows-hello]
strategy:
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
windows-hello: x86_64-pc-windows-msvc
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
windows-hello: aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
permissions:
contents: write
Expand All @@ -120,8 +179,15 @@ jobs:
echo '[target.${{ matrix.target }}]' >> .cargo/config
echo 'strip = { path = "${{ matrix.target }}-strip" }' >> .cargo/config
echo 'objcopy = { path = "${{ matrix.target }}-objcopy" }' >> .cargo/config
- uses: actions/download-artifact@v4
with:
name: "mairu-windows-hello-${{ matrix.windows-hello }}"
path: target/windows-hello
- name: pin the Windows Hello helper
shell: bash
run: echo "MAIRU_WINDOWS_HELLO_HELPER_SHA256=$(sha256sum target/windows-hello/mairu-windows-hello.exe | cut -d' ' -f1)" >> "$GITHUB_ENV"
- run: 'cargo install cargo-deb --locked'
- run: 'cargo deb --target=${{ matrix.target }}'
- run: 'cargo deb --target=${{ matrix.target }} --variant=release'
- uses: actions/attest-build-provenance@v4
with:
subject-path: "target/${{ matrix.target }}/debian/*.deb"
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
- A hardened agent can approve requests by a signature from your SSH agent at `$SSH_AUTH_SOCK`, including one forwarded with `ssh -A`: register public keys as `ssh_agent_keys` in `authorization.json` and approve the file once. Each challenge is single use and bound to the requesting process and request; FIDO `sk-` keys must assert user presence, and optionally user verification. A stale or unreachable socket falls back to polkit. Once registered keys are approved, the next policy change needs a signature by one of them. `mairu doctor` shows whether `SSH_AUTH_SOCK` is reachable and which registered keys it holds.
- `mairu login` opens the authorize URL in your browser, and clients post desktop notifications when a `mairu exec` needs a login, when an AI agent's request is refused, and when the policy is in lockdown. Each client picks `auto`, `agent` (the agent's helper, through the desktop portal or `xdg-open` and `org.freedesktop.Notifications` on the client's session), `command` (your own program) or `none`, through `--no-browser`, `MAIRU_OPEN_URL`/`MAIRU_NOTIFY` (`*_COMMAND`), or `open_url`/`notify` in `config.json`. `auto` honors `$BROWSER`, and over SSH without a display nothing is shown. `mairu doctor` shows the modes and what the agent can deliver.
- On WSL2, the agent tells the integrated profile (Windows interop on, where any program running as you can become root through `wsl.exe -u root`) from the isolated one. `mairu harden` warns and shows how to turn interop off, `mairu doctor` shows the WSL version, interop, profile and SSH agent, and policies can refuse roles while interop is on with `context.wsl_interop`. With interop on, `agent` mode opens URLs in the Windows default browser. WSL1 cannot be hardened.
- On WSL2 with interop on, a hardened agent can approve requests and policy changes by a signature from a Windows Hello key (PIN, fingerprint or face): `mairu policy enroll-windows-hello` creates the key and prints the `windows_hello_keys` entry for `authorization.json`. Linux release archives and the Debian package ship the signer `mairu-windows-hello.exe` and pin its SHA-256; builds from source use `windows_hello_helper` in `config.json` and `windows_hello_helper_sha256` in `authorization.json`. Cancelling the Windows Hello dialog denies the request. `mairu doctor` shows the helper and the key.
- The agent runs platform services in a child process, `mairu platform-helper`, that holds no tokens. The agent and the helper verify each other, the helper exits with the agent, and a helper that died is restarted within 30 seconds; meanwhile those services fail closed. `mairu doctor` shows the helper's state and capabilities.

### Miscellaneous

- macOS binaries are now published per architecture as `mairu-x86_64-apple-darwin.tar.gz` and `mairu-aarch64-apple-darwin.tar.gz`. The universal binary `mairu-universal-apple-darwin.tar.gz` is no longer published.
- Debug builds use a separate `dev` profile (`mairu-dev-agent.sock`, `~/.local/state/mairu-dev`), so a development build no longer talks to the installed agent. `MAIRU_PROFILE` overrides it.
- Releases carry `mairu-windows-hello-x86_64-pc-windows-msvc.zip` and `mairu-windows-hello-aarch64-pc-windows-msvc.zip`, and Linux archives grow by the Windows Hello helper. New `windows-hello-helper` Cargo feature builds it: `cargo build --target x86_64-pc-windows-msvc --no-default-features --features windows-hello-helper --bin mairu-windows-hello`.
- New `platform-helper` Cargo feature builds a standalone `mairu-platform-helper` binary without the client and agent: `cargo build --no-default-features --features platform-helper --bin mairu-platform-helper`.
- The client and agent are behind a new default `cli` Cargo feature. `native-tls` and `rustls` imply it, so existing build commands such as `--no-default-features --features rustls` still build the full CLI.

Expand Down
53 changes: 53 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 25 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ assets = [
]
maintainer-scripts = "pkg/debian/"

# Release debs, with the Windows Hello helper the release workflow puts in target/windows-hello/.
[package.metadata.deb.variants.release]
name = "mairu"
assets = [
["target/release/mairu", "usr/bin/", "755"],
["pkg/polkit/co.0w0.mairu.policy", "usr/share/polkit-1/actions/", "644"],
["target/windows-hello/mairu-windows-hello.exe", "usr/lib/mairu/", "755"],
]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
shadow-rs = { version = "2", default-features = false, features = ["build"] }
Expand Down Expand Up @@ -65,6 +74,7 @@ cli = [
"dep:oauth2",
"dep:process_path",
"dep:reqwest",
"dep:rsa",
"dep:serde_with",
"dep:sigstore-trust-root",
"dep:sigstore-verify",
Expand All @@ -84,6 +94,8 @@ rustls = ["cli", "reqwest/rustls-tls-native-roots", "sigstore-trust-root?/rustls
platform-services = ["dep:zbus", "dep:zbus_polkit", "tokio/process"]
# The standalone helper binary, built without `cli`.
platform-helper = ["platform-services"]
# `mairu-windows-hello.exe`, built for Windows without `cli`.
windows-hello-helper = ["dep:windows"]

[[bin]]
name = "mairu"
Expand All @@ -95,6 +107,11 @@ name = "mairu-platform-helper"
path = "src/bin/mairu_platform_helper.rs"
required-features = ["platform-helper"]

[[bin]]
name = "mairu-windows-hello"
path = "src/bin/mairu_windows_hello.rs"
required-features = ["windows-hello-helper"]

[dependencies]
anyhow = { version = "1.0.100", features = ["backtrace"] }
async-recursion = { version = "1.1.1", optional = true }
Expand Down Expand Up @@ -122,7 +139,6 @@ hyper = { version = "1.8.1", features = ["http1", "server"], optional = true }
hyper-util = { version = "0.1.19", optional = true }
indoc = { version = "2.0.7", optional = true }
libc = "0.2.178"
nix = { version = "0.30.1", features = ["resource", "process", "ptrace", "mman", "fs", "signal", "hostname", "user", "socket", "time"] }
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest"], optional = true }
once_cell = { version = "1.21.3", features = ["parking_lot"] }
parking_lot = "0.12.5"
Expand Down Expand Up @@ -153,6 +169,11 @@ shadow-rs = { version = "2", default-features = false }
# ssh-agent presence: SSHSIG verification and the agent protocol.
ssh-encoding = { version = "0.2.0", features = ["std"], optional = true }
ssh-key = { version = "0.6.7", default-features = false, features = ["std", "ed25519", "p256", "p384", "p521", "rsa"], optional = true }
# windows-hello presence: RSA PKCS #1 v1.5 verification.
rsa = { version = "0.9.10", default-features = false, features = ["std", "sha2"], optional = true }

[target.'cfg(unix)'.dependencies]
nix = { version = "0.30.1", features = ["resource", "process", "ptrace", "mman", "fs", "signal", "hostname", "user", "socket", "time"] }

# `mairu harden --from` verifies release attestations.
[target.'cfg(target_os = "linux")'.dependencies]
Expand All @@ -166,6 +187,9 @@ zbus_polkit = { version = "6.0.0", default-features = false, features = ["tokio"
[target.'cfg(any(target_os = "freebsd", target_os = "dragonfly"))'.dependencies]
rustix = { version = "1.1.2", default-features = false, features = ["std", "process"] }

[target.'cfg(windows)'.dependencies]
windows = { version = "0.62.2", default-features = false, features = ["std", "Foundation", "Security_Credentials", "Security_Cryptography", "Security_Cryptography_Core", "Storage_Streams", "Win32_Foundation", "Win32_UI_WindowsAndMessaging"], optional = true }

[dev-dependencies]
# Signature fixtures for sk- keys, which no SSH agent library signs without hardware.
ed25519-dalek = "2.2.0"
Expand Down
Loading
Loading