From 593c5f8f7ff7a7a841c66fedb5353ff166fffa42 Mon Sep 17 00:00:00 2001 From: inx Date: Wed, 28 Jan 2026 15:56:54 +0800 Subject: [PATCH] fix: critical patches for ssh signing, profile switching, and subdir support fix: critical patches for ssh signing, profile switching, and subdir support This release addresses several critical issues discovered during E2E testing: - **fix(exec):** Prevent crash in Exec Mode when using SSH signing. - Sanitizer now resets `gpg.format` to "openpgp" and `gpg.ssh.program` to "ssh-keygen" instead of empty strings (which caused Git to exit with code 128). - **fix(switch):** Correctly replace active profile instead of appending. - Now cleans up existing Gosh include paths before adding the new one to prevent ambiguous identity resolution. - **fix(core):** Support running Gosh from deep subdirectories. - Replaced manual `.git` folder check with `git rev-parse` to correctly detect repository root. - **security:** Hardened Blind Injection defaults to align with Git 2.52+ strictness. --- Cargo.toml | 32 +++--- scripts/build.sh | 200 ++++++++++++++++----------------- scripts/tests/README.md | 59 ++++++++++ scripts/tests/alice.sh | 179 +++++++++++++++++++++++++++++ scripts/tests/alice2.sh | 153 +++++++++++++++++++++++++ scripts/tests/alice3.sh | 187 ++++++++++++++++++++++++++++++ scripts/tests/edge_cases.sh | 73 ++++++++++++ scripts/tests/security_edge.sh | 50 +++++++++ src/git.rs | 72 ++++++++---- src/sanitizer.rs | 5 +- 10 files changed, 873 insertions(+), 137 deletions(-) mode change 100755 => 100644 scripts/build.sh create mode 100644 scripts/tests/README.md create mode 100755 scripts/tests/alice.sh create mode 100755 scripts/tests/alice2.sh create mode 100755 scripts/tests/alice3.sh create mode 100755 scripts/tests/edge_cases.sh create mode 100755 scripts/tests/security_edge.sh diff --git a/Cargo.toml b/Cargo.toml index 8fe1aae..83f9c8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,16 @@ -[package] -name = "gosh" -version = "0.1.0" -edition = "2021" - -[dependencies] -clap = { version = "4.5", features = ["derive"] } -serde = { version = "1.0", features = ["derive"] } -toml = "0.8" -dirs = "5.0" -anyhow = "1.0" - -[dev-dependencies] -assert_cmd = "2.0" -tempfile = "3.10" -predicates = "3.1" +[package] +name = "gosh" +version = "0.1.1" +edition = "2021" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" +dirs = "5.0" +anyhow = "1.0" + +[dev-dependencies] +assert_cmd = "2.0" +tempfile = "3.10" +predicates = "3.1" diff --git a/scripts/build.sh b/scripts/build.sh old mode 100755 new mode 100644 index eea3a6f..b04b686 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,101 +1,101 @@ -#!/bin/bash -set -e # 遇到错误立即停止 - -# --- 配置部分 --- -APP_NAME="gosh" -OUTPUT_DIR="dist" -# 新增:获取 dist 目录的绝对物理路径 -mkdir -p "$OUTPUT_DIR" -ABS_OUTPUT_DIR="$(realpath "$OUTPUT_DIR")" - -# 你想要支持的 Target 列表 -# cross 支持的列表可见: https://github.com/cross-rs/cross#supported-targets -TARGETS=( - "x86_64-unknown-linux-gnu" # 标准 Linux x64 - "x86_64-unknown-linux-musl" # 静态链接 Linux x64 (推荐: 无依赖,兼容性最好) - # "i686-unknown-linux-gnu" # Linux x86 - # "i586-unknown-linux-musl" # Linux x86 (静态链接) - "aarch64-unknown-linux-gnu" # Linux ARM64 (如树莓派 4, docker ARM 容器) - # "x86_64-unknown-freebsd" # FreeBSD x64 - # "x86_64-pc-windows-gnu" # Windows x64 (使用 MinGW) - # "aarch64-apple-darwin" # macOS ARM64 (注意: cross 对 macOS 支持有限,通常建议在 Mac 上原生编译) -) - -# --- 检查依赖 --- -if ! command -v cross &> /dev/null; then - echo "❌ Error: 'cross' is not installed." - echo "👉 Please install it: cargo install cross" - exit 1 -fi - -if ! command -v podman &> /dev/null; then - echo "❌ Error: 'podman' is not installed or not in PATH." - exit 1 -fi - -# --- 获取版本号 --- -# 简单地从 Cargo.toml 提取版本号 -VERSION=$(grep "^version" Cargo.toml | head -n 1 | cut -d '"' -f 2) -echo "🚀 Preparing to build $APP_NAME v$VERSION..." - -# 清理并创建输出目录 -rm -rf "$OUTPUT_DIR" -mkdir -p "$OUTPUT_DIR" - -# --- 循环编译 --- -for target in "${TARGETS[@]}"; do - echo "------------------------------------------------" - echo "🔨 Building target: $target" - echo "------------------------------------------------" - - # 1. 使用 cross 编译 release 版本 - cross build --target "$target" --release - - # 2. 准备打包 - BINARY_NAME="$APP_NAME" - if [[ $target == *"windows"* ]]; then - BINARY_NAME="${APP_NAME}.exe" - fi - - # 查找编译生成的二进制文件位置 - BUILD_BIN_PATH="target/$target/release/$BINARY_NAME" - - if [ ! -f "$BUILD_BIN_PATH" ]; then - echo "❌ Error: Binary not found at $BUILD_BIN_PATH" - exit 1 - fi - - # 3. 打包文件名格式: gosh-v0.1.0-x86_64-unknown-linux-musl.tar.gz - ARCHIVE_NAME="${APP_NAME}-v${VERSION}-${target}" - - # 进入输出目录进行打包操作 - # 创建一个临时目录来存放二进制文件和文档(如果有 README/LICENSE) - TMP_DIR=$(mktemp -d) - cp "$BUILD_BIN_PATH" "$TMP_DIR/" - # 如果有 README 或 LICENSE,也可以 cp 到 TMP_DIR - # cp README.md LICENSE "$TMP_DIR/" - - echo "📦 Packaging $ARCHIVE_NAME..." - - # if [[ $target == *"windows"* ]]; then - # # Windows 使用 zip - # ARCHIVE_FILE="${ARCHIVE_NAME}.zip" - # (cd "$TMP_DIR" && zip -r "../../$OUTPUT_DIR/$ARCHIVE_FILE" .) - # else - # Linux/Unix 使用 tar.gz - ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz" - (cd "$TMP_DIR" && tar -czf "$ABS_OUTPUT_DIR/$ARCHIVE_FILE" .) - # fi - - # 清理临时目录 - rm -rf "$TMP_DIR" - - # 4. 生成校验和 (SHA256) - (cd "$ABS_OUTPUT_DIR" && shasum -a 256 "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.sha256") - - echo "✅ Success: $OUTPUT_DIR/$ARCHIVE_FILE" -done - -echo "------------------------------------------------" -echo "🎉 Build finished! All artifacts are in '$OUTPUT_DIR/'" +#!/bin/bash +set -e # 遇到错误立即停止 + +# --- 配置部分 --- +APP_NAME="gosh" +OUTPUT_DIR="dist" +# 新增:获取 dist 目录的绝对物理路径 +mkdir -p "$OUTPUT_DIR" +ABS_OUTPUT_DIR="$(realpath "$OUTPUT_DIR")" + +# 你想要支持的 Target 列表 +# cross 支持的列表可见: https://github.com/cross-rs/cross#supported-targets +TARGETS=( + "x86_64-unknown-linux-gnu" # 标准 Linux x64 + "x86_64-unknown-linux-musl" # 静态链接 Linux x64 (推荐: 无依赖,兼容性最好) + # "i686-unknown-linux-gnu" # Linux x86 + # "i586-unknown-linux-musl" # Linux x86 (静态链接) + "aarch64-unknown-linux-gnu" # Linux ARM64 (如树莓派 4, docker ARM 容器) + # "x86_64-unknown-freebsd" # FreeBSD x64 + # "x86_64-pc-windows-gnu" # Windows x64 (使用 MinGW) + # "aarch64-apple-darwin" # macOS ARM64 (注意: cross 对 macOS 支持有限,通常建议在 Mac 上原生编译) +) + +# --- 检查依赖 --- +if ! command -v cross &> /dev/null; then + echo "❌ Error: 'cross' is not installed." + echo "👉 Please install it: cargo install cross" + exit 1 +fi + +if ! command -v podman &> /dev/null; then + echo "❌ Error: 'podman' is not installed or not in PATH." + exit 1 +fi + +# --- 获取版本号 --- +# 简单地从 Cargo.toml 提取版本号 +VERSION=$(grep "^version" Cargo.toml | head -n 1 | cut -d '"' -f 2) +echo "🚀 Preparing to build $APP_NAME v$VERSION..." + +# 清理并创建输出目录 +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" + +# --- 循环编译 --- +for target in "${TARGETS[@]}"; do + echo "------------------------------------------------" + echo "🔨 Building target: $target" + echo "------------------------------------------------" + + # 1. 使用 cross 编译 release 版本 + cross build --target "$target" --release + + # 2. 准备打包 + BINARY_NAME="$APP_NAME" + if [[ $target == *"windows"* ]]; then + BINARY_NAME="${APP_NAME}.exe" + fi + + # 查找编译生成的二进制文件位置 + BUILD_BIN_PATH="target/$target/release/$BINARY_NAME" + + if [ ! -f "$BUILD_BIN_PATH" ]; then + echo "❌ Error: Binary not found at $BUILD_BIN_PATH" + exit 1 + fi + + # 3. 打包文件名格式: gosh-v0.1.0-x86_64-unknown-linux-musl.tar.gz + ARCHIVE_NAME="${APP_NAME}-v${VERSION}-${target}" + + # 进入输出目录进行打包操作 + # 创建一个临时目录来存放二进制文件和文档(如果有 README/LICENSE) + TMP_DIR=$(mktemp -d) + cp "$BUILD_BIN_PATH" "$TMP_DIR/" + # 如果有 README 或 LICENSE,也可以 cp 到 TMP_DIR + # cp README.md LICENSE "$TMP_DIR/" + + echo "📦 Packaging $ARCHIVE_NAME..." + + # if [[ $target == *"windows"* ]]; then + # # Windows 使用 zip + # ARCHIVE_FILE="${ARCHIVE_NAME}.zip" + # (cd "$TMP_DIR" && zip -r "../../$OUTPUT_DIR/$ARCHIVE_FILE" .) + # else + # Linux/Unix 使用 tar.gz + ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz" + (cd "$TMP_DIR" && tar -czf "$ABS_OUTPUT_DIR/$ARCHIVE_FILE" .) + # fi + + # 清理临时目录 + rm -rf "$TMP_DIR" + + # 4. 生成校验和 (SHA256) + (cd "$ABS_OUTPUT_DIR" && shasum -a 256 "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.sha256") + + echo "✅ Success: $OUTPUT_DIR/$ARCHIVE_FILE" +done + +echo "------------------------------------------------" +echo "🎉 Build finished! All artifacts are in '$OUTPUT_DIR/'" ls -lh "$OUTPUT_DIR" \ No newline at end of file diff --git a/scripts/tests/README.md b/scripts/tests/README.md new file mode 100644 index 0000000..a45af4c --- /dev/null +++ b/scripts/tests/README.md @@ -0,0 +1,59 @@ +# Gosh Integration & Scenario Tests + +This directory contains the integration test suite and scenario simulations for `gosh`. These scripts are designed to validate the core functionality, security isolation, and developer workflows in a controlled, sandbox environment. + +## Overview + +The tests in this directory focus on end-to-end (E2E) validation, ensuring that `gosh` correctly interacts with Git configurations, SSH keys, and system environments without contaminating the user's global settings. + +## Test Matrix + +| Script | Focus Area | Description | +| :--- | :--- | :--- | +| `alice.sh` | **Basic Workflow** | Validates profile creation, cloning, and identity switching with SSH signing. | +| `alice2.sh` | **Advanced Signing** | Extends basic tests with complex signing scenarios and multi-profile setups. | +| `alice3.sh` | **Full Lifecycle** | Simulates a complex developer lifecycle involving multiple organizations. | +| `edge_cases.sh`| **Robustness** | Tests boundary conditions, invalid inputs, and error handling. | +| `security_edge.sh`| **Security** | Validates configuration isolation and prevents "leakage" between profiles. | + +## Prerequisites + +To run these tests locally, ensure you have the following installed: +- **Bash**: Most scripts use standard Bash features. +- **Git**: Version 2.34+ is required for SSH signing tests. +- **Gosh**: The `gosh` binary must be available in your PATH or accessible via the `GOSH_CMD` environment variable. + +## Running Tests + +### Preparation + +Before running the tests, compile the project to ensure the latest binary is used: + +```bash +cargo build --release +export PATH="$PWD/target/release:$PATH" +``` + +### Execution + +You can run individual test scripts directly: + +```bash +bash scripts/tests/alice.sh +``` + +Each script initializes a sandbox in `/tmp/gosh_test_*` or similar, ensuring that your `~/.gitconfig` and `~/.ssh` remain untouched. + +## Design Principles + +1. **Isolation**: All tests use a dedicated `GOSH_CONFIG_PATH` and temporary directories to ensure side-effect-free execution. +2. **Assertions**: Scripts use exit codes and explicit checks to verify expected outcomes (e.g., checking `git cat-file` for signatures). +3. **Readability**: Log levels (STEP, INFO, ERROR) are used to provide clear feedback during execution. + +## Adding New Tests + +When adding a new test script: +- Use `set -e` to ensure the script fails on the first error. +- Use a dedicated sandbox directory for all temporary files. +- Export `GOSH_CONFIG_PATH` to point into your sandbox. +- Document the scenario and expected results in the script header. \ No newline at end of file diff --git a/scripts/tests/alice.sh b/scripts/tests/alice.sh new file mode 100755 index 0000000..ae391aa --- /dev/null +++ b/scripts/tests/alice.sh @@ -0,0 +1,179 @@ +#!/bin/bash +set -e + +# --- 0. 环境与工具准备 --- +GOSH_CMD="gosh" # 确保已编译或 alias 到 cargo run +BASE_DIR="/tmp/alice_demo_signed" + +# 隔离 Gosh 配置 +export GOSH_CONFIG_PATH="$BASE_DIR/config" +# 隔离 SSH 密钥目录 +SSH_DIR="$BASE_DIR/ssh_keys" +# 模拟仓库目录 +REPO_DIR="$BASE_DIR/repos" + +# 颜色定义 +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' + +log() { echo -e "${BLUE}[STEP]${NC} $1"; } +info() { echo -e "${GREEN} ->${NC} $1"; } +err() { echo -e "${RED} -> ERROR:${NC} $1"; exit 1; } + +# 检查 Git 版本 (SSH 签名需要 Git 2.34+) +GIT_VERSION=$(git --version | awk '{print $3}') +info "Git Version: $GIT_VERSION (SSH Signing requires 2.34+)" + +# --- 1. 清理与沙盒初始化 --- +log "Initializing Sandbox at $BASE_DIR..." +rm -rf "$BASE_DIR" +mkdir -p "$GOSH_CONFIG_PATH" +mkdir -p "$SSH_DIR" +mkdir -p "$REPO_DIR" + +# --- 2. 生成隔离的 SSH 密钥对 (模拟 Work 和 Personal) --- +log "Generating isolated SSH keys..." + +# 生成 Work Key (无密码) +ssh-keygen -t ed25519 -C "alice@contoso.com" -f "$SSH_DIR/id_work" -N "" -q +info "Generated Work Key: $SSH_DIR/id_work" + +# 生成 Personal Key (无密码) +ssh-keygen -t ed25519 -C "alice@alice.com" -f "$SSH_DIR/id_personal" -N "" -q +info "Generated Personal Key: $SSH_DIR/id_personal" + +# --- 3. 使用 Gosh 创建 Profile 并注入签名配置 --- +log "Creating Gosh Profiles..." + +# 3.1 创建基础 Work Profile +$GOSH_CMD -c "Alice Work" "alice@contoso.com" "work" + +# 3.2 手动追加 SSH 签名配置到 Work Profile +# 这里演示了 Gosh 的灵活性:你可以手动编辑生成的 .gitconfig +WORK_PROFILE="$GOSH_CONFIG_PATH/profiles/work.gitconfig" +cat >> "$WORK_PROFILE" <> "$PERSONAL_PROFILE" < Infer -> Switch) +# 注意:这里我们 Clone 本地路径,但 core.sshCommand 依然会被配置进去,这是符合预期的 +$GOSH_CMD work clone "$REPO_DIR/backend.git" work-backend +cd work-backend + +# 提交代码 +touch work.txt +git add work.txt +git commit -m "Work commit" > /dev/null + +# 验证签名 +# 检查 raw commit data 中是否包含 gpgsig 字段 +if git cat-file commit HEAD | grep -q "gpgsig"; then + info "✅ Commit is SIGNED." +else + err "Commit is NOT signed." +fi + +# 验证使用的是哪个 Key +SIGNER_KEY=$(git config user.signingkey) +if [[ "$SIGNER_KEY" == *"/id_work.pub" ]]; then + info "✅ Signed with WORK Key." +else + err "Wrong key used: $SIGNER_KEY" +fi + +# === 场景 B: 切换身份并验证签名 (Switch Mode) === +log "Scenario B: Switch Mode (Existing Repo)" +cd "$REPO_DIR" +git init --quiet "oss-project" +cd oss-project + +# 切换到 Personal +$GOSH_CMD personal + +# 提交 +touch fun.txt +git add fun.txt +git commit -m "Personal commit" > /dev/null + +# 验证签名 +if git cat-file commit HEAD | grep -q "gpgsig"; then + info "✅ Commit is SIGNED." +else + err "Commit is NOT signed." +fi + +SIGNER_KEY=$(git config user.signingkey) +if [[ "$SIGNER_KEY" == *"/id_personal.pub" ]]; then + info "✅ Signed with PERSONAL Key." +else + err "Wrong key used: $SIGNER_KEY" +fi + +# === 场景 C: 临时执行与密钥隔离 (Exec Mode) === +log "Scenario C: Ephemeral Execution (Security Check)" +# 当前在 oss-project (Personal),我们想用 Work 身份签个名 + +# 执行 gosh work commit +$GOSH_CMD work commit --allow-empty -m "Hotfix via Exec" > /dev/null + +# 验证最后一次提交的签名 +# 注意:Exec 模式下,Gosh 会通过 -c user.signingkey="" 先清空,再注入 work profile +# 如果这一步成功且签名了,说明 Gosh 正确注入了 id_work.pub + +LATEST_COMMIT_MSG=$(git log -1 --pretty=%B) +info "Latest commit: $LATEST_COMMIT_MSG" + +# 这里的验证比较 tricky,因为 git log 不会直接显示是哪个 key 文件签的 +# 但如果签名成功,且 Email 是 Work,基本证明逻辑通了 +AUTHOR=$(git log -1 --pretty=format:'%ae') +if [ "$AUTHOR" == "alice@contoso.com" ]; then + info "✅ Ephemeral commit author is Correct (Work)." +else + err "Ephemeral commit author mismatch: $AUTHOR" +fi + +if git cat-file commit HEAD | grep -q "gpgsig"; then + info "✅ Ephemeral commit is SIGNED (Injection worked)." +else + err "Ephemeral commit failed to sign (Injection failed)." +fi + +# --- 5. 清理 --- +log "Done. To inspect, check $BASE_DIR before exiting." +# rm -rf "$BASE_DIR" # 注释掉此行以便你检查文件 +echo -e "${GREEN}🎉 Demo completed without touching ~/.ssh or ~/.gnupg!${NC}" \ No newline at end of file diff --git a/scripts/tests/alice2.sh b/scripts/tests/alice2.sh new file mode 100755 index 0000000..8e0596f --- /dev/null +++ b/scripts/tests/alice2.sh @@ -0,0 +1,153 @@ +#!/bin/bash +set -e + +# --- 0. 环境与工具准备 --- +GOSH_CMD="gosh" # 确保已编译或 alias 到 cargo run +BASE_DIR="/tmp/alice_demo_debug" + +# 隔离 Gosh 配置 +export GOSH_CONFIG_PATH="$BASE_DIR/config" +# 隔离 SSH 密钥目录 +SSH_DIR="$BASE_DIR/ssh_keys" +# 模拟仓库目录 +REPO_DIR="$BASE_DIR/repos" + +# 颜色定义 +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log() { echo -e "\n${BLUE}[STEP]${NC} $1"; } +info() { echo -e "${GREEN} ->${NC} $1"; } +err() { echo -e "${RED} -> ERROR:${NC} $1"; exit 1; } + +# === 调试核心函数 === +debug_inspect() { + echo -e "${YELLOW}--- 🔍 DEBUG INSPECTION ---${NC}" + echo -e "${YELLOW}[1. Local Config (.git/config)]${NC}" + # 只显示相关的配置 + git config --local --list | grep -E "user|include|core.sshCommand|gpg" || echo " (Clean/No local config overrides)" + + echo -e "${YELLOW}[2. Latest Commit Details]${NC}" + # 显示签名、作者、提交者 + git log -1 --show-signature --pretty=fuller + echo -e "${YELLOW}---------------------------${NC}" +} + +# 检查 Git 版本 +GIT_VERSION=$(git --version | awk '{print $3}') +info "Git Version: $GIT_VERSION (SSH Signing requires 2.34+)" + +# --- 1. 清理与沙盒初始化 --- +log "Initializing Sandbox at $BASE_DIR..." +rm -rf "$BASE_DIR" +mkdir -p "$GOSH_CONFIG_PATH" +mkdir -p "$SSH_DIR" +mkdir -p "$REPO_DIR" + +# --- 2. 生成隔离的 SSH 密钥对 --- +log "Generating isolated SSH keys..." +ssh-keygen -t ed25519 -C "alice@contoso.com" -f "$SSH_DIR/id_work" -N "" -q +info "Generated Work Key: .../id_work" +ssh-keygen -t ed25519 -C "alice@alice.com" -f "$SSH_DIR/id_personal" -N "" -q +info "Generated Personal Key: .../id_personal" + +# --- 3. 使用 Gosh 创建 Profile --- +log "Creating Gosh Profiles..." + +# 3.1 Work Profile +$GOSH_CMD -c "Alice Work" "alice@contoso.com" "work" +WORK_PROFILE="$GOSH_CONFIG_PATH/profiles/work.gitconfig" +cat >> "$WORK_PROFILE" <> "$PERSONAL_PROFILE" <> "$ALLOWED_SIGNERS" +ok "Generated Alice's Key & Added to Trust Store" + +# Bob (Partner) +ssh-keygen -t ed25519 -C "bob@partner.org" -f "$SSH_DIR/id_bob" -N "" -q +echo "bob@partner.org $(cat $SSH_DIR/id_bob.pub)" >> "$ALLOWED_SIGNERS" +ok "Generated Bob's Key & Added to Trust Store" + +# --- 3. 配置 Gosh Profiles --- +log "Configuring Gosh Profiles..." + +# --> Alice Profile +$GOSH_CMD -c "Alice Work" "alice@contoso.com" "alice_work" +cat >> "$GOSH_CONFIG_PATH/profiles/alice_work.gitconfig" < Bob Profile +$GOSH_CMD -c "Bob Partner" "bob@partner.org" "bob_partner" +cat >> "$GOSH_CONFIG_PATH/profiles/bob_partner.gitconfig" <>> [Commit 1] Alice starts the project" +$GOSH_CMD alice_work +touch README.md +git add README.md +git commit -m "Init Project Alpha" > /dev/null +verify_last_commit "alice@contoso.com" "alice_work" + +# 2. Bob 进来修改 (模拟同一台机器切换身份) +echo ">>> [Commit 2] Bob adds features" +$GOSH_CMD bob_partner +echo "Feature by Bob" >> README.md +git commit -am "Bob adds feature" > /dev/null +verify_last_commit "bob@partner.org" "bob_partner" + +# 3. Alice 审查并修改 +echo ">>> [Commit 3] Alice reviews and updates" +$GOSH_CMD alice_work +echo "Reviewed by Alice" >> README.md +git commit -am "Alice review" > /dev/null +verify_last_commit "alice@contoso.com" "alice_work" + + +# === 项目 B: Exec 模式 (临时介入) === +log "Scenario 2: Project Beta (Exec Mode Intervention)" +cd "$REPO_DIR" +git init --quiet project-beta +cd project-beta + +# 1. Bob 拥有这个项目 +$GOSH_CMD bob_partner +touch main.rs +git add main.rs +git commit -m "Bob starts Beta" > /dev/null +verify_last_commit "bob@partner.org" "bob_partner" + +# 2. Alice 临时修复 (不切换 Profile,直接用 Exec) +# 当前 Profile 依然是 bob_partner (可以通过 .git/config 验证) +# Alice 用 gosh alice_work exec 临时提交 +echo ">>> [Commit 4] Alice hotfixes via Exec Mode" +echo "// Hotfix" >> main.rs +git add main.rs + +# 这里是关键测试:Exec 模式下的签名注入 +$GOSH_CMD alice_work commit -m "Alice hotfix" > /dev/null + +# 验证:虽然此时 .git/config 指向 Bob,但这个 Commit 必须是 Alice 签名的 +verify_last_commit "alice@contoso.com" "alice_work" + +# 3. 再次确认环境没被污染 +# 此时如果不加参数执行 git commit,应该依然是 Bob +echo ">>> [Check] Verifying environment reset to Bob" +touch bob.txt +git add bob.txt +git commit -m "Bob continues" > /dev/null +verify_last_commit "bob@partner.org" "bob_partner" + + +# --- 5. 最终检查 --- +log "Summary" +echo "Checking Project Alpha Log (Should show Alice -> Bob -> Alice):" +cd "$REPO_DIR/project-alpha" +git log --pretty=format:'%C(yellow)%h%Creset - %C(green)%an%Creset (%C(blue)%G?%Creset) : %s' --graph +echo "" + +echo -e "\n${PASS}🎉 All collaboration scenarios verified with strict signature checking!${NC}" \ No newline at end of file diff --git a/scripts/tests/edge_cases.sh b/scripts/tests/edge_cases.sh new file mode 100755 index 0000000..15a9ee8 --- /dev/null +++ b/scripts/tests/edge_cases.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -e + +# --- 配置 --- +GOSH_CMD="gosh" +BASE_DIR="/tmp/gosh_edge_test" +export GOSH_CONFIG_PATH="$BASE_DIR/config" +REPO_DIR="$BASE_DIR/repos" + +# 颜色 +PASS='\033[0;32m' +FAIL='\033[0;31m' +NC='\033[0m' +log() { echo -e "\n\033[0;34m[TEST] $1\033[0m"; } + +# --- 初始化 --- +rm -rf "$BASE_DIR" +mkdir -p "$GOSH_CONFIG_PATH" "$REPO_DIR" + +# 创建一个 Profile +log "Creating Profile..." +$GOSH_CMD -c "Edge User" "edge@test.com" "edge" + +# --- 测试 1: 子目录执行 --- +log "Scenario 1: Running from a deep subdirectory" +cd "$REPO_DIR" +git init --quiet deep-repo +cd deep-repo +mkdir -p src/deep/level +cd src/deep/level + +echo "Current dir: $(pwd)" +echo "Executing 'gosh edge' from subdirectory..." + +# 执行 switch +$GOSH_CMD edge + +# 验证 +# 我们需要回到根目录看 config,或者直接用 git config +CONFIG_EMAIL=$(git config user.email) +if [ "$CONFIG_EMAIL" == "edge@test.com" ]; then + echo -e "${PASS}✓ Subdirectory switch worked!${NC}" +else + echo -e "${FAIL}✗ Failed! Git config not updated correctly from subdir.${NC}" + exit 1 +fi + +# --- 测试 2: 带空格的路径 --- +log "Scenario 2: Repository path with SPACES" +cd "$REPO_DIR" +# 创建带空格的目录 +DIR_WITH_SPACE="My Cool Project" +mkdir "$DIR_WITH_SPACE" +cd "$DIR_WITH_SPACE" +git init --quiet + +echo "Current dir: $(pwd)" +echo "Executing 'gosh edge'..." + +$GOSH_CMD edge + +# 验证 +CONFIG_EMAIL=$(git config user.email) +if [ "$CONFIG_EMAIL" == "edge@test.com" ]; then + echo -e "${PASS}✓ Path with spaces worked!${NC}" +else + echo -e "${FAIL}✗ Failed! Path with spaces broke the include.${NC}" + # 调试信息:打印出 config 看看路径变成啥样了 + cat .git/config + exit 1 +fi + +echo -e "\n${PASS}🎉 All Edge Cases Passed! v1.0 is ready to ship.${NC}" \ No newline at end of file diff --git a/scripts/tests/security_edge.sh b/scripts/tests/security_edge.sh new file mode 100755 index 0000000..cfb4287 --- /dev/null +++ b/scripts/tests/security_edge.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# --- 准备 --- +GOSH_CMD="gosh" # 确保已编译或 alias +BASE_DIR="/tmp/gosh_security_test" +UNSAFE_REPO="$BASE_DIR/root_owned_repo" + +# 1. 初始化一个归属于 root 的仓库 (对当前用户来说是不安全的) +rm -rf "$BASE_DIR" +mkdir -p "$UNSAFE_REPO" + +echo "[SETUP] Creating a repo owned by ROOT..." +# 使用 sudo 创建 .git,这样它就属于 root 了 +sudo git init --quiet "$UNSAFE_REPO" +sudo touch "$UNSAFE_REPO/testfile" + +# 确保当前用户对目录有读写权限(以便能进入),但 .git 依然属于 root +sudo chmod -R 777 "$UNSAFE_REPO" + +echo "[TEST] Running 'gosh' in a dubious ownership repo..." +cd "$UNSAFE_REPO" + +# 2. 尝试运行 gosh (期望失败) +if $GOSH_CMD -l > /dev/null 2>&1; then + # 注意:gosh -l 不需要 git 仓库,所以应该成功。 + # 我们需要测 switch 或 exec,这需要 git 上下文 + echo " (gosh list works, which is fine)" +fi + +echo "Attempting to switch profile..." +# 捕获输出 +OUTPUT=$($GOSH_CMD testprofile 2>&1 || true) + +# 3. 验证结果 +if echo "$OUTPUT" | grep -q "fatal: detected dubious ownership"; then + echo "✅ PASS: Gosh propagated Git's security error." + echo " Git said: 'detected dubious ownership'" + echo " Gosh refused to act." +elif echo "$OUTPUT" | grep -q "Not a git repository"; then + echo "✅ PASS: Gosh treated it as invalid (Git rev-parse failed)." +else + echo "❌ FAIL: Gosh tried to execute! This is dangerous." + echo "Output was: $OUTPUT" + exit 1 +fi + +# 清理 (需要 sudo 因为文件夹是 root 的) +cd /tmp +sudo rm -rf "$BASE_DIR" +echo "🎉 Security verification complete." \ No newline at end of file diff --git a/src/git.rs b/src/git.rs index fe9bbee..be5a551 100644 --- a/src/git.rs +++ b/src/git.rs @@ -70,6 +70,40 @@ fn run_command(cmd: &mut Command) -> Result<()> { Ok(()) } +fn get_profile_dir(config: &GoshConfig) -> Result { + expand_path(&config.profile_dir) +} + +fn clean_existing_profiles(profile_dir: &Path) -> Result<()> { + let output = Command::new("git") + .args(&["config", "--local", "--get-all", "include.path"]) + .output()?; + + if !output.status.success() { + return Ok(()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + + for line in stdout.lines() { + let val = line.trim(); + // Check if the path belongs to our profile directory + // We use string containment as a heuristic since paths might be canonicalized differently + // but typically we add absolute paths. + let is_gosh_profile = val.contains(&profile_dir.to_string_lossy().to_string()) + || (val.contains("/profiles/") && val.ends_with(".gitconfig")); + + if is_gosh_profile { + let mut cmd = Command::new("git"); + cmd.args(&["config", "--local", "--unset", "include.path", val]); + // We tolerate failure here (e.g. if key doesn't exist anymore for some reason) + let _ = cmd.output(); + } + } + + Ok(()) +} + fn run_exec(config: &GoshConfig, profile_id: &str, args: &[String]) -> Result<()> { let profile_path = get_profile_path(config, profile_id)?; let injections = sanitizer::get_blind_injections(); @@ -95,8 +129,16 @@ fn run_exec(config: &GoshConfig, profile_id: &str, args: &[String]) -> Result<() } fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()> { - if !Path::new(".git").exists() { - return Err(anyhow!("Not a git repository (checked current directory)")); + let status = Command::new("git") + .args(&["rev-parse", "--is-inside-work-tree"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + + let is_git_repo = status.map(|s| s.success()).unwrap_or(false); + + if !is_git_repo { + return Err(anyhow!("Not a git repository")); } let profile_path = get_profile_path(config, profile_id)?; @@ -135,24 +177,16 @@ fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()> } } - // Add include path - // We should check if it exists? "The system SHOULD check if the include already exists" - // git config --get-all include.path - let output = Command::new("git") - .args(&["config", "--get-all", "include.path"]) - .output()?; - - let current_includes = String::from_utf8_lossy(&output.stdout); + // 0. Clean existing gosh profiles + let profiles_dir = get_profile_dir(config)?; + clean_existing_profiles(&profiles_dir)?; + + // 1. Add new profile let path_str = abs_profile_path.to_string_lossy(); - - if !current_includes.contains(&*path_str) { - let mut cmd = Command::new("git"); - cmd.args(&["config", "--add", "include.path", &path_str]); - run_command(&mut cmd)?; - println!("Switched to profile '{}'", profile_id); - } else { - println!("Profile '{}' already active", profile_id); - } + let mut cmd = Command::new("git"); + cmd.args(&["config", "--local", "--add", "include.path", &path_str]); + run_command(&mut cmd)?; + println!("Switched to profile '{}'", profile_id); Ok(()) } diff --git a/src/sanitizer.rs b/src/sanitizer.rs index b76ff7d..1164d6b 100644 --- a/src/sanitizer.rs +++ b/src/sanitizer.rs @@ -13,8 +13,9 @@ pub fn get_blind_injections() -> Vec<(&'static str, &'static str)> { ("user.email", ""), ("user.signingkey", ""), ("core.sshCommand", ""), - ("gpg.format", ""), - ("gpg.ssh.program", ""), + ("gpg.format", "openpgp"), + ("gpg.ssh.program", "ssh-keygen"), + ("gpg.program", "gpg"), ("commit.gpgsign", "false"), ("tag.gpgsign", "false"), ]