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.
This commit is contained in:
parent
fe173e7d4a
commit
fd892ac59c
10 changed files with 873 additions and 137 deletions
32
Cargo.toml
32
Cargo.toml
|
|
@ -1,16 +1,16 @@
|
||||||
[package]
|
[package]
|
||||||
name = "gosh"
|
name = "gosh"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
predicates = "3.1"
|
predicates = "3.1"
|
||||||
|
|
|
||||||
200
scripts/build.sh
Executable file → Normal file
200
scripts/build.sh
Executable file → Normal file
|
|
@ -1,101 +1,101 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e # 遇到错误立即停止
|
set -e # 遇到错误立即停止
|
||||||
|
|
||||||
# --- 配置部分 ---
|
# --- 配置部分 ---
|
||||||
APP_NAME="gosh"
|
APP_NAME="gosh"
|
||||||
OUTPUT_DIR="dist"
|
OUTPUT_DIR="dist"
|
||||||
# 新增:获取 dist 目录的绝对物理路径
|
# 新增:获取 dist 目录的绝对物理路径
|
||||||
mkdir -p "$OUTPUT_DIR"
|
mkdir -p "$OUTPUT_DIR"
|
||||||
ABS_OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
|
ABS_OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
|
||||||
|
|
||||||
# 你想要支持的 Target 列表
|
# 你想要支持的 Target 列表
|
||||||
# cross 支持的列表可见: https://github.com/cross-rs/cross#supported-targets
|
# cross 支持的列表可见: https://github.com/cross-rs/cross#supported-targets
|
||||||
TARGETS=(
|
TARGETS=(
|
||||||
"x86_64-unknown-linux-gnu" # 标准 Linux x64
|
"x86_64-unknown-linux-gnu" # 标准 Linux x64
|
||||||
"x86_64-unknown-linux-musl" # 静态链接 Linux x64 (推荐: 无依赖,兼容性最好)
|
"x86_64-unknown-linux-musl" # 静态链接 Linux x64 (推荐: 无依赖,兼容性最好)
|
||||||
# "i686-unknown-linux-gnu" # Linux x86
|
# "i686-unknown-linux-gnu" # Linux x86
|
||||||
# "i586-unknown-linux-musl" # Linux x86 (静态链接)
|
# "i586-unknown-linux-musl" # Linux x86 (静态链接)
|
||||||
"aarch64-unknown-linux-gnu" # Linux ARM64 (如树莓派 4, docker ARM 容器)
|
"aarch64-unknown-linux-gnu" # Linux ARM64 (如树莓派 4, docker ARM 容器)
|
||||||
# "x86_64-unknown-freebsd" # FreeBSD x64
|
# "x86_64-unknown-freebsd" # FreeBSD x64
|
||||||
# "x86_64-pc-windows-gnu" # Windows x64 (使用 MinGW)
|
# "x86_64-pc-windows-gnu" # Windows x64 (使用 MinGW)
|
||||||
# "aarch64-apple-darwin" # macOS ARM64 (注意: cross 对 macOS 支持有限,通常建议在 Mac 上原生编译)
|
# "aarch64-apple-darwin" # macOS ARM64 (注意: cross 对 macOS 支持有限,通常建议在 Mac 上原生编译)
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- 检查依赖 ---
|
# --- 检查依赖 ---
|
||||||
if ! command -v cross &> /dev/null; then
|
if ! command -v cross &> /dev/null; then
|
||||||
echo "❌ Error: 'cross' is not installed."
|
echo "❌ Error: 'cross' is not installed."
|
||||||
echo "👉 Please install it: cargo install cross"
|
echo "👉 Please install it: cargo install cross"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v podman &> /dev/null; then
|
if ! command -v podman &> /dev/null; then
|
||||||
echo "❌ Error: 'podman' is not installed or not in PATH."
|
echo "❌ Error: 'podman' is not installed or not in PATH."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 获取版本号 ---
|
# --- 获取版本号 ---
|
||||||
# 简单地从 Cargo.toml 提取版本号
|
# 简单地从 Cargo.toml 提取版本号
|
||||||
VERSION=$(grep "^version" Cargo.toml | head -n 1 | cut -d '"' -f 2)
|
VERSION=$(grep "^version" Cargo.toml | head -n 1 | cut -d '"' -f 2)
|
||||||
echo "🚀 Preparing to build $APP_NAME v$VERSION..."
|
echo "🚀 Preparing to build $APP_NAME v$VERSION..."
|
||||||
|
|
||||||
# 清理并创建输出目录
|
# 清理并创建输出目录
|
||||||
rm -rf "$OUTPUT_DIR"
|
rm -rf "$OUTPUT_DIR"
|
||||||
mkdir -p "$OUTPUT_DIR"
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
# --- 循环编译 ---
|
# --- 循环编译 ---
|
||||||
for target in "${TARGETS[@]}"; do
|
for target in "${TARGETS[@]}"; do
|
||||||
echo "------------------------------------------------"
|
echo "------------------------------------------------"
|
||||||
echo "🔨 Building target: $target"
|
echo "🔨 Building target: $target"
|
||||||
echo "------------------------------------------------"
|
echo "------------------------------------------------"
|
||||||
|
|
||||||
# 1. 使用 cross 编译 release 版本
|
# 1. 使用 cross 编译 release 版本
|
||||||
cross build --target "$target" --release
|
cross build --target "$target" --release
|
||||||
|
|
||||||
# 2. 准备打包
|
# 2. 准备打包
|
||||||
BINARY_NAME="$APP_NAME"
|
BINARY_NAME="$APP_NAME"
|
||||||
if [[ $target == *"windows"* ]]; then
|
if [[ $target == *"windows"* ]]; then
|
||||||
BINARY_NAME="${APP_NAME}.exe"
|
BINARY_NAME="${APP_NAME}.exe"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 查找编译生成的二进制文件位置
|
# 查找编译生成的二进制文件位置
|
||||||
BUILD_BIN_PATH="target/$target/release/$BINARY_NAME"
|
BUILD_BIN_PATH="target/$target/release/$BINARY_NAME"
|
||||||
|
|
||||||
if [ ! -f "$BUILD_BIN_PATH" ]; then
|
if [ ! -f "$BUILD_BIN_PATH" ]; then
|
||||||
echo "❌ Error: Binary not found at $BUILD_BIN_PATH"
|
echo "❌ Error: Binary not found at $BUILD_BIN_PATH"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. 打包文件名格式: gosh-v0.1.0-x86_64-unknown-linux-musl.tar.gz
|
# 3. 打包文件名格式: gosh-v0.1.0-x86_64-unknown-linux-musl.tar.gz
|
||||||
ARCHIVE_NAME="${APP_NAME}-v${VERSION}-${target}"
|
ARCHIVE_NAME="${APP_NAME}-v${VERSION}-${target}"
|
||||||
|
|
||||||
# 进入输出目录进行打包操作
|
# 进入输出目录进行打包操作
|
||||||
# 创建一个临时目录来存放二进制文件和文档(如果有 README/LICENSE)
|
# 创建一个临时目录来存放二进制文件和文档(如果有 README/LICENSE)
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
cp "$BUILD_BIN_PATH" "$TMP_DIR/"
|
cp "$BUILD_BIN_PATH" "$TMP_DIR/"
|
||||||
# 如果有 README 或 LICENSE,也可以 cp 到 TMP_DIR
|
# 如果有 README 或 LICENSE,也可以 cp 到 TMP_DIR
|
||||||
# cp README.md LICENSE "$TMP_DIR/"
|
# cp README.md LICENSE "$TMP_DIR/"
|
||||||
|
|
||||||
echo "📦 Packaging $ARCHIVE_NAME..."
|
echo "📦 Packaging $ARCHIVE_NAME..."
|
||||||
|
|
||||||
# if [[ $target == *"windows"* ]]; then
|
# if [[ $target == *"windows"* ]]; then
|
||||||
# # Windows 使用 zip
|
# # Windows 使用 zip
|
||||||
# ARCHIVE_FILE="${ARCHIVE_NAME}.zip"
|
# ARCHIVE_FILE="${ARCHIVE_NAME}.zip"
|
||||||
# (cd "$TMP_DIR" && zip -r "../../$OUTPUT_DIR/$ARCHIVE_FILE" .)
|
# (cd "$TMP_DIR" && zip -r "../../$OUTPUT_DIR/$ARCHIVE_FILE" .)
|
||||||
# else
|
# else
|
||||||
# Linux/Unix 使用 tar.gz
|
# Linux/Unix 使用 tar.gz
|
||||||
ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz"
|
ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz"
|
||||||
(cd "$TMP_DIR" && tar -czf "$ABS_OUTPUT_DIR/$ARCHIVE_FILE" .)
|
(cd "$TMP_DIR" && tar -czf "$ABS_OUTPUT_DIR/$ARCHIVE_FILE" .)
|
||||||
# fi
|
# fi
|
||||||
|
|
||||||
# 清理临时目录
|
# 清理临时目录
|
||||||
rm -rf "$TMP_DIR"
|
rm -rf "$TMP_DIR"
|
||||||
|
|
||||||
# 4. 生成校验和 (SHA256)
|
# 4. 生成校验和 (SHA256)
|
||||||
(cd "$ABS_OUTPUT_DIR" && shasum -a 256 "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.sha256")
|
(cd "$ABS_OUTPUT_DIR" && shasum -a 256 "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.sha256")
|
||||||
|
|
||||||
echo "✅ Success: $OUTPUT_DIR/$ARCHIVE_FILE"
|
echo "✅ Success: $OUTPUT_DIR/$ARCHIVE_FILE"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "------------------------------------------------"
|
echo "------------------------------------------------"
|
||||||
echo "🎉 Build finished! All artifacts are in '$OUTPUT_DIR/'"
|
echo "🎉 Build finished! All artifacts are in '$OUTPUT_DIR/'"
|
||||||
ls -lh "$OUTPUT_DIR"
|
ls -lh "$OUTPUT_DIR"
|
||||||
59
scripts/tests/README.md
Normal file
59
scripts/tests/README.md
Normal file
|
|
@ -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.
|
||||||
179
scripts/tests/alice.sh
Executable file
179
scripts/tests/alice.sh
Executable file
|
|
@ -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" <<EOF
|
||||||
|
[gpg]
|
||||||
|
format = ssh
|
||||||
|
[user]
|
||||||
|
signingkey = $SSH_DIR/id_work.pub
|
||||||
|
[commit]
|
||||||
|
gpgsign = true
|
||||||
|
[core]
|
||||||
|
# 强制 SSH 使用指定的私钥,且忽略用户本机的 ~/.ssh/config
|
||||||
|
sshCommand = ssh -i $SSH_DIR/id_work -F /dev/null -o IdentitiesOnly=yes -o StrictHostKeyChecking=no
|
||||||
|
EOF
|
||||||
|
info "Configured Work Profile with SSH Signing"
|
||||||
|
|
||||||
|
# 3.3 创建并配置 Personal Profile
|
||||||
|
$GOSH_CMD -c "Alice Personal" "alice@alice.com" "personal"
|
||||||
|
PERSONAL_PROFILE="$GOSH_CONFIG_PATH/profiles/personal.gitconfig"
|
||||||
|
cat >> "$PERSONAL_PROFILE" <<EOF
|
||||||
|
[gpg]
|
||||||
|
format = ssh
|
||||||
|
[user]
|
||||||
|
signingkey = $SSH_DIR/id_personal.pub
|
||||||
|
[commit]
|
||||||
|
gpgsign = true
|
||||||
|
[core]
|
||||||
|
sshCommand = ssh -i $SSH_DIR/id_personal -F /dev/null -o IdentitiesOnly=yes -o StrictHostKeyChecking=no
|
||||||
|
EOF
|
||||||
|
info "Configured Personal Profile with SSH Signing"
|
||||||
|
|
||||||
|
# --- 4. 场景测试 ---
|
||||||
|
|
||||||
|
# === 场景 A: 克隆并验证签名 (Setup Mode) ===
|
||||||
|
log "Scenario A: Setup Mode (Work Repo)"
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
# 模拟远程仓库
|
||||||
|
git init --bare --quiet "backend.git"
|
||||||
|
|
||||||
|
# 使用 Gosh 克隆 (Clone -> 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}"
|
||||||
153
scripts/tests/alice2.sh
Executable file
153
scripts/tests/alice2.sh
Executable file
|
|
@ -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" <<EOF
|
||||||
|
[gpg]
|
||||||
|
format = ssh
|
||||||
|
[user]
|
||||||
|
signingkey = $SSH_DIR/id_work.pub
|
||||||
|
[commit]
|
||||||
|
gpgsign = true
|
||||||
|
[core]
|
||||||
|
sshCommand = ssh -i $SSH_DIR/id_work -F /dev/null -o IdentitiesOnly=yes -o StrictHostKeyChecking=no
|
||||||
|
EOF
|
||||||
|
info "Configured Work Profile (SSH Signing Enabled)"
|
||||||
|
|
||||||
|
# 3.2 Personal Profile
|
||||||
|
$GOSH_CMD -c "Alice Personal" "alice@alice.com" "personal"
|
||||||
|
PERSONAL_PROFILE="$GOSH_CONFIG_PATH/profiles/personal.gitconfig"
|
||||||
|
cat >> "$PERSONAL_PROFILE" <<EOF
|
||||||
|
[gpg]
|
||||||
|
format = ssh
|
||||||
|
[user]
|
||||||
|
signingkey = $SSH_DIR/id_personal.pub
|
||||||
|
[commit]
|
||||||
|
gpgsign = true
|
||||||
|
[core]
|
||||||
|
sshCommand = ssh -i $SSH_DIR/id_personal -F /dev/null -o IdentitiesOnly=yes -o StrictHostKeyChecking=no
|
||||||
|
EOF
|
||||||
|
info "Configured Personal Profile (SSH Signing Enabled)"
|
||||||
|
|
||||||
|
# --- 4. 场景测试 ---
|
||||||
|
|
||||||
|
# === 场景 A: 克隆并验证签名 (Setup Mode) ===
|
||||||
|
log "Scenario A: Setup Mode (Work Repo)"
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
git init --bare --quiet "backend.git"
|
||||||
|
|
||||||
|
# 使用 Gosh 克隆
|
||||||
|
info "Running: gosh work clone ..."
|
||||||
|
$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 (Scenario A)"
|
||||||
|
|
||||||
|
# 🔍 查看日志
|
||||||
|
debug_inspect
|
||||||
|
|
||||||
|
# === 场景 B: 切换身份并验证签名 (Switch Mode) ===
|
||||||
|
log "Scenario B: Switch Mode (Existing Repo)"
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
git init --quiet "oss-project"
|
||||||
|
cd oss-project
|
||||||
|
|
||||||
|
# 切换到 Personal
|
||||||
|
info "Running: gosh personal (Switching...)"
|
||||||
|
$GOSH_CMD personal
|
||||||
|
|
||||||
|
# 提交
|
||||||
|
touch fun.txt
|
||||||
|
git add fun.txt
|
||||||
|
git commit -m "Personal commit (Scenario B)"
|
||||||
|
|
||||||
|
# 🔍 查看日志
|
||||||
|
debug_inspect
|
||||||
|
|
||||||
|
# === 场景 C: 临时执行与密钥隔离 (Exec Mode) ===
|
||||||
|
log "Scenario C: Ephemeral Execution (Security Check)"
|
||||||
|
info "Current Profile is: Personal (oss-project)"
|
||||||
|
info "Executing 'gosh work commit' (Should use Work Identity temporarily)..."
|
||||||
|
|
||||||
|
# 执行 gosh work commit
|
||||||
|
# 注意:这里我们不再重定向到 /dev/null,我们要看 git 的原生输出
|
||||||
|
$GOSH_CMD work commit --allow-empty -m "Hotfix via Exec (Scenario C)"
|
||||||
|
|
||||||
|
# 🔍 查看日志
|
||||||
|
# 这里的重点是:
|
||||||
|
# 1. Author 必须是 Work
|
||||||
|
# 2. 签名必须有效 (Good signature)
|
||||||
|
# 3. 但 Local Config (上面显示的 [1]) 必须依然显示 Personal 的 include
|
||||||
|
debug_inspect
|
||||||
|
|
||||||
|
# 验证持久配置未变
|
||||||
|
if grep -q "personal.gitconfig" .git/config; then
|
||||||
|
info "✅ Persistent config verification: Still using 'personal' profile."
|
||||||
|
else
|
||||||
|
err "Persistent config was altered!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 5. 清理 ---
|
||||||
|
log "Done. Check the debug logs above."
|
||||||
|
echo -e "${GREEN}🎉 Debug run completed.${NC}"
|
||||||
187
scripts/tests/alice3.sh
Executable file
187
scripts/tests/alice3.sh
Executable file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# --- 0. 全局配置 ---
|
||||||
|
GOSH_CMD="gosh"
|
||||||
|
BASE_DIR="/tmp/gosh_collab_demo"
|
||||||
|
|
||||||
|
# 隔离 Gosh 配置
|
||||||
|
export GOSH_CONFIG_PATH="$BASE_DIR/config"
|
||||||
|
# 隔离 SSH 密钥目录
|
||||||
|
SSH_DIR="$BASE_DIR/ssh_keys"
|
||||||
|
# 模拟仓库目录
|
||||||
|
REPO_DIR="$BASE_DIR/repos"
|
||||||
|
# 信任文件路径 (用于校验签名)
|
||||||
|
ALLOWED_SIGNERS="$BASE_DIR/allowed_signers"
|
||||||
|
|
||||||
|
# 颜色
|
||||||
|
PASS='\033[0;32m'
|
||||||
|
INFO='\033[0;34m'
|
||||||
|
FAIL='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "\n${INFO}[STEP]${NC} $1"; }
|
||||||
|
ok() { echo -e "${PASS} ✓${NC} $1"; }
|
||||||
|
err() { echo -e "${FAIL} ✗ ERROR:${NC} $1"; exit 1; }
|
||||||
|
|
||||||
|
# --- 校验核心函数 ---
|
||||||
|
# 用法: verify_last_commit "期望的邮箱" "期望的Profile名"
|
||||||
|
verify_last_commit() {
|
||||||
|
EXPECTED_EMAIL="$1"
|
||||||
|
PROFILE_NAME="$2"
|
||||||
|
|
||||||
|
# 获取最后一次提交的签名状态 (%G?) 和 签名者邮箱 (%GS) 和 作者邮箱 (%ae)
|
||||||
|
# %G? : G=Good, B=Bad, U=Untrusted, N=None
|
||||||
|
STATS=$(git log -1 --pretty=format:'%G?|%ae')
|
||||||
|
SIG_STATUS=${STATS%%|*}
|
||||||
|
AUTHOR_EMAIL=${STATS##*|}
|
||||||
|
|
||||||
|
echo -ne " Verifying Commit... "
|
||||||
|
|
||||||
|
# 1. 验证作者
|
||||||
|
if [ "$AUTHOR_EMAIL" != "$EXPECTED_EMAIL" ]; then
|
||||||
|
echo ""
|
||||||
|
err "Author mismatch! Expected $EXPECTED_EMAIL, got $AUTHOR_EMAIL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. 验证签名有效性
|
||||||
|
if [ "$SIG_STATUS" == "G" ]; then
|
||||||
|
echo -e "${PASS}[Signature: GOOD]${NC} ${PASS}[Author: MATCH]${NC}"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
# 打印详细日志帮助调试
|
||||||
|
git log -1 --show-signature
|
||||||
|
err "Signature verification failed! Status code: $SIG_STATUS (Expected 'G')"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1. 初始化沙盒 ---
|
||||||
|
log "Initializing Sandbox..."
|
||||||
|
rm -rf "$BASE_DIR"
|
||||||
|
mkdir -p "$GOSH_CONFIG_PATH/profiles"
|
||||||
|
mkdir -p "$SSH_DIR"
|
||||||
|
mkdir -p "$REPO_DIR"
|
||||||
|
|
||||||
|
# 全局配置 Git (仅在沙盒内) 以启用 SSH 签名验证
|
||||||
|
# 这一步解决了之前 'No signature' / 'allowedSignersFile' 的问题
|
||||||
|
git config --global gpg.ssh.allowedSignersFile "$ALLOWED_SIGNERS"
|
||||||
|
|
||||||
|
# --- 2. 生成密钥并建立信任链 ---
|
||||||
|
log "Generating Keys & Establishing Trust..."
|
||||||
|
|
||||||
|
# Alice (Work)
|
||||||
|
ssh-keygen -t ed25519 -C "alice@contoso.com" -f "$SSH_DIR/id_alice" -N "" -q
|
||||||
|
echo "alice@contoso.com $(cat $SSH_DIR/id_alice.pub)" >> "$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" <<EOF
|
||||||
|
[gpg]
|
||||||
|
format = ssh
|
||||||
|
[user]
|
||||||
|
signingkey = $SSH_DIR/id_alice.pub
|
||||||
|
[commit]
|
||||||
|
gpgsign = true
|
||||||
|
[core]
|
||||||
|
sshCommand = ssh -i $SSH_DIR/id_alice -F /dev/null -o IdentitiesOnly=yes -o StrictHostKeyChecking=no
|
||||||
|
EOF
|
||||||
|
ok "Profile 'alice_work' created"
|
||||||
|
|
||||||
|
# --> Bob Profile
|
||||||
|
$GOSH_CMD -c "Bob Partner" "bob@partner.org" "bob_partner"
|
||||||
|
cat >> "$GOSH_CONFIG_PATH/profiles/bob_partner.gitconfig" <<EOF
|
||||||
|
[gpg]
|
||||||
|
format = ssh
|
||||||
|
[user]
|
||||||
|
signingkey = $SSH_DIR/id_bob.pub
|
||||||
|
[commit]
|
||||||
|
gpgsign = true
|
||||||
|
[core]
|
||||||
|
sshCommand = ssh -i $SSH_DIR/id_bob -F /dev/null -o IdentitiesOnly=yes -o StrictHostKeyChecking=no
|
||||||
|
EOF
|
||||||
|
ok "Profile 'bob_partner' created"
|
||||||
|
|
||||||
|
|
||||||
|
# --- 4. 模拟协作流程 ---
|
||||||
|
|
||||||
|
# === 项目 A: 交互式开发 (交叉签名) ===
|
||||||
|
log "Scenario 1: Project Alpha (Intersection)"
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
git init --quiet project-alpha
|
||||||
|
cd project-alpha
|
||||||
|
|
||||||
|
# 1. Alice 初始化项目
|
||||||
|
echo ">>> [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}"
|
||||||
73
scripts/tests/edge_cases.sh
Executable file
73
scripts/tests/edge_cases.sh
Executable file
|
|
@ -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}"
|
||||||
50
scripts/tests/security_edge.sh
Executable file
50
scripts/tests/security_edge.sh
Executable file
|
|
@ -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."
|
||||||
72
src/git.rs
72
src/git.rs
|
|
@ -70,6 +70,40 @@ fn run_command(cmd: &mut Command) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_profile_dir(config: &GoshConfig) -> Result<PathBuf> {
|
||||||
|
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<()> {
|
fn run_exec(config: &GoshConfig, profile_id: &str, args: &[String]) -> Result<()> {
|
||||||
let profile_path = get_profile_path(config, profile_id)?;
|
let profile_path = get_profile_path(config, profile_id)?;
|
||||||
let injections = sanitizer::get_blind_injections();
|
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<()> {
|
fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()> {
|
||||||
if !Path::new(".git").exists() {
|
let status = Command::new("git")
|
||||||
return Err(anyhow!("Not a git repository (checked current directory)"));
|
.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)?;
|
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
|
// 0. Clean existing gosh profiles
|
||||||
// We should check if it exists? "The system SHOULD check if the include already exists"
|
let profiles_dir = get_profile_dir(config)?;
|
||||||
// git config --get-all include.path
|
clean_existing_profiles(&profiles_dir)?;
|
||||||
let output = Command::new("git")
|
|
||||||
.args(&["config", "--get-all", "include.path"])
|
// 1. Add new profile
|
||||||
.output()?;
|
|
||||||
|
|
||||||
let current_includes = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let path_str = abs_profile_path.to_string_lossy();
|
let path_str = abs_profile_path.to_string_lossy();
|
||||||
|
let mut cmd = Command::new("git");
|
||||||
if !current_includes.contains(&*path_str) {
|
cmd.args(&["config", "--local", "--add", "include.path", &path_str]);
|
||||||
let mut cmd = Command::new("git");
|
run_command(&mut cmd)?;
|
||||||
cmd.args(&["config", "--add", "include.path", &path_str]);
|
println!("Switched to profile '{}'", profile_id);
|
||||||
run_command(&mut cmd)?;
|
|
||||||
println!("Switched to profile '{}'", profile_id);
|
|
||||||
} else {
|
|
||||||
println!("Profile '{}' already active", profile_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ pub fn get_blind_injections() -> Vec<(&'static str, &'static str)> {
|
||||||
("user.email", ""),
|
("user.email", ""),
|
||||||
("user.signingkey", ""),
|
("user.signingkey", ""),
|
||||||
("core.sshCommand", ""),
|
("core.sshCommand", ""),
|
||||||
("gpg.format", ""),
|
("gpg.format", "openpgp"),
|
||||||
("gpg.ssh.program", ""),
|
("gpg.ssh.program", "ssh-keygen"),
|
||||||
|
("gpg.program", "gpg"),
|
||||||
("commit.gpgsign", "false"),
|
("commit.gpgsign", "false"),
|
||||||
("tag.gpgsign", "false"),
|
("tag.gpgsign", "false"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue