Skip to content

Commit 2fc38b8

Browse files
Rewrite in Rust (#2)
1 parent 63bfe41 commit 2fc38b8

43 files changed

Lines changed: 5302 additions & 2427 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: cargo
4+
directory: /
5+
schedule:
6+
interval: daily
7+
time: "06:00"
8+
timezone: America/Los_Angeles
9+
commit-message:
10+
prefix: "deps:"
11+
labels:
12+
- dependencies
13+
open-pull-requests-limit: 10
14+
groups:
15+
minor-and-patch:
16+
update-types:
17+
- minor
18+
- patch
19+
major:
20+
update-types:
21+
- major
22+
23+
- package-ecosystem: github-actions
24+
directory: /
25+
schedule:
26+
interval: daily
27+
time: "06:00"
28+
timezone: America/Los_Angeles
29+
commit-message:
30+
prefix: "deps(actions):"
31+
labels:
32+
- dependencies
33+
open-pull-requests-limit: 5
34+
groups:
35+
minor-and-patch:
36+
update-types:
37+
- minor
38+
- patch
39+
major:
40+
update-types:
41+
- major

.github/release-drafter.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name-template: "v$RESOLVED_VERSION"
2+
tag-template: "v$RESOLVED_VERSION"
3+
categories:
4+
- title: "Breaking Changes"
5+
label: "breaking-change"
6+
- title: "Dependencies"
7+
collapse-after: 1
8+
labels:
9+
- "dependencies"
10+
11+
version-resolver:
12+
major:
13+
labels:
14+
- "major"
15+
- "breaking-change"
16+
minor:
17+
labels:
18+
- "minor"
19+
- "new-feature"
20+
patch:
21+
labels:
22+
- "bugfix"
23+
- "dependencies"
24+
- "documentation"
25+
- "enhancement"
26+
default: patch
27+
28+
template: |
29+
## What's Changed
30+
31+
$CHANGES
32+
33+
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
name: Test
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19+
- name: Install Rust toolchain
20+
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable
21+
with:
22+
toolchain: stable
23+
- name: Cache cargo
24+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
25+
with:
26+
path: |
27+
~/.cargo/registry
28+
~/.cargo/git
29+
target
30+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
31+
restore-keys: ${{ runner.os }}-cargo-
32+
- name: Install ssh-keygen (for SSHSIG byte-equality tests)
33+
run: sudo apt-get update && sudo apt-get install -y openssh-client
34+
- name: Test
35+
run: cargo test

.github/workflows/lint.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Lint
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
check:
14+
name: Clippy & Format
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19+
- name: Install Rust toolchain
20+
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable
21+
with:
22+
toolchain: stable
23+
components: clippy, rustfmt
24+
- name: Cache cargo
25+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
26+
with:
27+
path: |
28+
~/.cargo/registry
29+
~/.cargo/git
30+
target
31+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
32+
restore-keys: ${{ runner.os }}-cargo-
33+
- name: Format check
34+
run: cargo fmt --all -- --check
35+
- name: Clippy
36+
run: cargo clippy -- -D warnings
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Release Drafter
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
jobs:
13+
release-drafter:
14+
name: Release Drafter
15+
environment: release-drafter
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7
19+
id: drafter
20+
21+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
22+
with:
23+
token: ${{ secrets.RELEASE_TOKEN }}
24+
25+
- name: Update version in Cargo.toml
26+
env:
27+
VERSION: ${{ steps.drafter.outputs.resolved_version }}
28+
run: |
29+
sed -i "s/^version = .*/version = \"${VERSION}\"/" Cargo.toml
30+
31+
- name: Regenerate lockfile
32+
run: cargo generate-lockfile
33+
34+
- name: Commit changes
35+
env:
36+
VERSION: ${{ steps.drafter.outputs.resolved_version }}
37+
run: |
38+
if ! git diff --quiet; then
39+
git config --global user.name "github-actions[bot]"
40+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
41+
git commit -am "Bump version to ${VERSION} [skip ci]"
42+
git push
43+
fi

.github/workflows/release.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
build:
12+
name: Build ${{ matrix.target }}
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
include:
18+
- target: x86_64-unknown-linux-gnu
19+
os: ubuntu-latest
20+
artifact: ssh-agent-proxy
21+
- target: aarch64-apple-darwin
22+
os: macos-latest
23+
artifact: ssh-agent-proxy
24+
- target: x86_64-pc-windows-msvc
25+
os: windows-latest
26+
artifact: ssh-agent-proxy.exe
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30+
31+
- name: Install Rust toolchain
32+
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable
33+
with:
34+
toolchain: stable
35+
targets: ${{ matrix.target }}
36+
37+
- name: Cache cargo
38+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
39+
with:
40+
path: |
41+
~/.cargo/registry
42+
~/.cargo/git
43+
target
44+
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
45+
restore-keys: ${{ runner.os }}-${{ matrix.target }}-cargo-
46+
47+
- name: Build
48+
run: cargo build --release --target ${{ matrix.target }}
49+
50+
- name: Package (Unix)
51+
if: runner.os != 'Windows'
52+
env:
53+
TARGET: ${{ matrix.target }}
54+
run: |
55+
cd target/${TARGET}/release
56+
tar czf ../../../ssh-agent-proxy-${TARGET}.tar.gz ssh-agent-proxy
57+
cd ../../..
58+
sha256sum ssh-agent-proxy-${TARGET}.tar.gz > ssh-agent-proxy-${TARGET}.tar.gz.sha256
59+
60+
- name: Package (Windows)
61+
if: runner.os == 'Windows'
62+
env:
63+
TARGET: ${{ matrix.target }}
64+
run: |
65+
cd target/$env:TARGET/release
66+
Compress-Archive -Path ssh-agent-proxy.exe -DestinationPath ../../../ssh-agent-proxy-$env:TARGET.zip
67+
cd ../../..
68+
(Get-FileHash -Algorithm SHA256 "ssh-agent-proxy-$env:TARGET.zip").Hash.ToLower() + " ssh-agent-proxy-$env:TARGET.zip" | Out-File -Encoding ascii "ssh-agent-proxy-$env:TARGET.zip.sha256"
69+
70+
- name: Upload release assets
71+
env:
72+
GH_TOKEN: ${{ github.token }}
73+
TAG: ${{ github.event.release.tag_name }}
74+
TARGET: ${{ matrix.target }}
75+
run: gh release upload "${TAG}" ssh-agent-proxy-${TARGET}.* --clobber
76+
shell: bash

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ go.work.sum
3030
# env file
3131
.env
3232

33+
# Rust build artifacts
34+
/target/
35+
3336
# Editor/IDE
3437
# .idea/
3538
# .vscode/

CLAUDE.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# CLAUDE.md
2+
3+
## Project
4+
5+
ssh-agent-proxy — a localhost HTTP signing proxy backed by any ssh-agent.
6+
Written in Rust (edition 2024). Single binary, no runtime dependencies.
7+
8+
## Build & test
9+
10+
```sh
11+
cargo build --release # Linux
12+
cargo test # requires ssh-keygen for SSHSIG byte-equality tests
13+
cargo clippy -- -D warnings # lint
14+
cargo fmt -- --check # format check
15+
make build-windows # cross-compile (needs mingw)
16+
```
17+
18+
## Architecture
19+
20+
- `src/main.rs` — entry point, tokio runtime, signal handling, service dispatch
21+
- `src/server.rs` — axum HTTP handlers (/sign, /publickey, /healthz)
22+
- `src/config.rs` — env var config loading
23+
- `src/sshsig.rs` — SSHSIG wire format (must produce output byte-identical to ssh-keygen)
24+
- `src/agent.rs` — minimal SSH agent protocol client (LIST + SIGN only)
25+
- `src/agent_source.rs` — dials agent per request, key selection, RSA sha2-512 upgrade
26+
- `src/wire.rs` — shared SSH string read/write primitives
27+
- `src/dialer_{unix,windows}.rs` — platform-specific agent connection
28+
- `src/hardening_{linux,macos,windows}.rs` — process hardening (prctl, mlockall, etc.)
29+
- `src/service_windows.rs` — Windows SCM integration (install/uninstall/dispatcher)
30+
31+
## Key design decisions
32+
33+
- Fresh agent connection per HTTP request — no caching, no key material held between requests
34+
- `AgentBackedSigner` uses `Mutex` (not `RefCell`) for interior mutability because axum handlers require `Send`
35+
- Signature format anti-downgrade check on ALL key types, not just RSA
36+
- `DefaultBodyLimit` enforced at the axum layer, not just in-handler
37+
- Platform code uses `#[cfg(target_os)]` / `#[cfg(unix)]` / `#[cfg(windows)]`
38+
- Windows service module is in main.rs module tree (not lib.rs) because it calls `crate::run()`
39+
40+
## Testing
41+
42+
The SSHSIG byte-equality tests are the most important — they prove the wire format
43+
is correct by comparing against `ssh-keygen -Y sign` output. If those pass, the
44+
signing pipeline is correct.
45+
46+
## Cross-compilation
47+
48+
Windows cross-compile from Linux requires `x86_64-pc-windows-gnu` target and
49+
`gcc-mingw-w64-x86-64`. Native Windows builds use MSVC and avoid Smart App Control
50+
issues that MinGW binaries can trigger.

0 commit comments

Comments
 (0)