diff --git a/.github/scripts/sanity_check.sh b/.github/scripts/sanity_check.sh new file mode 100755 index 0000000..ff1c079 --- /dev/null +++ b/.github/scripts/sanity_check.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# ============================================================================= +# libsmx 安全性扫描脚本 +# ============================================================================= +# 被 CI 工作流调用,执行以下检查: +# 1. 扫描非测试代码中的 panic!/unwrap()/expect() +# 2. 检查是否有硬编码密钥泄露 +# 3. 验证 Cargo.toml 版本号与 Git Tag 匹配(仅在 tag 构建时) +# ============================================================================= + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +ERRORS=0 + +pass() { echo -e "${GREEN}[PASS]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +fail() { echo -e "${RED}[FAIL]${NC} $1"; ERRORS=$((ERRORS + 1)); } + +echo "==========================================" +echo " libsmx Security Sanity Checks" +echo "==========================================" +echo "" + +# ───────────────────────────────────────────────────────────────────────────── +# 1. 扫描非测试代码中的 panic!/unwrap()/expect() +# ───────────────────────────────────────────────────────────────────────────── +# Reason: 密码学库不应在非测试代码中 panic,应返回 Result/Option +echo "--- 检查 panic/unwrap/expect ---" + +# 排除规则: +# - #[cfg(test)] 块和 mod tests 块内的代码 +# - benches/ 和 tests/ 目录 +# - 白名单:try_into().unwrap() 用于固定大小切片转换(已知安全) + +# 收集 src/ 下所有非测试 .rs 文件中的匹配项 +PANIC_HITS=$(grep -rn 'panic!\|\.unwrap()\|\.expect(' src/ --include='*.rs' \ + | grep -v '#\[cfg(test)\]' \ + | grep -v 'mod tests' \ + | grep -v '// test' \ + | grep -v '#\[test\]' \ + | grep -v 'fn test_' \ + || true) + +if [ -n "$PANIC_HITS" ]; then + # 统计行数 + COUNT=$(echo "$PANIC_HITS" | wc -l) + + # 分类统计 + UNWRAP_COUNT=$(echo "$PANIC_HITS" | grep -c '\.unwrap()' || true) + EXPECT_COUNT=$(echo "$PANIC_HITS" | grep -c '\.expect(' || true) + PANIC_COUNT=$(echo "$PANIC_HITS" | grep -c 'panic!' || true) + + # try_into().unwrap() 是固定大小切片转换的惯用法,已知不会 panic + SAFE_UNWRAP=$(echo "$PANIC_HITS" | grep -c 'try_into().unwrap()' || true) + UNSAFE_UNWRAP=$((UNWRAP_COUNT - SAFE_UNWRAP)) + + if [ "$PANIC_COUNT" -gt 0 ]; then + fail "发现 $PANIC_COUNT 处 panic! 调用(非测试代码)" + echo "$PANIC_HITS" | grep 'panic!' | head -5 + fi + + if [ "$UNSAFE_UNWRAP" -gt 0 ]; then + warn "发现 $UNSAFE_UNWRAP 处 .unwrap() 调用(不含 try_into().unwrap())" + echo "$PANIC_HITS" | grep '\.unwrap()' | grep -v 'try_into().unwrap()' | head -5 + fi + + if [ "$EXPECT_COUNT" -gt 0 ]; then + warn "发现 $EXPECT_COUNT 处 .expect() 调用" + echo "$PANIC_HITS" | grep '\.expect(' | head -5 + fi + + if [ "$SAFE_UNWRAP" -gt 0 ]; then + echo -e " ${GREEN}(白名单)${NC} $SAFE_UNWRAP 处 try_into().unwrap()(固定大小切片转换,已知安全)" + fi +else + pass "非测试代码中无 panic/unwrap/expect" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# 2. 检查硬编码密钥/敏感信息 +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "--- 检查硬编码敏感信息 ---" + +# Reason: 防止私钥、密码等敏感信息被提交到仓库 +SENSITIVE_PATTERNS='(private.?key|secret.?key|password|passwd|api.?key|token)\s*=\s*["\x27][^\x27"]+["\x27]' +SENSITIVE_HITS=$(grep -rniE "$SENSITIVE_PATTERNS" src/ --include='*.rs' \ + | grep -v '#\[cfg(test)\]' \ + | grep -v 'mod tests' \ + | grep -v '// test' \ + | grep -v 'fn test_' \ + || true) + +if [ -n "$SENSITIVE_HITS" ]; then + warn "发现疑似硬编码敏感信息(请人工审查):" + echo "$SENSITIVE_HITS" | head -5 +else + pass "未发现硬编码敏感信息" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# 3. 验证 Cargo.toml 版本号与 Git Tag 匹配 +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "--- 检查版本号一致性 ---" + +CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') +echo " Cargo.toml version: $CARGO_VERSION" + +# 如果在 CI 中且有 GITHUB_REF_NAME(tag 构建),验证匹配 +if [ -n "${GITHUB_REF_NAME:-}" ] && [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then + TAG_VERSION="${GITHUB_REF_NAME#v}" + echo " Git tag version: $TAG_VERSION" + if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then + fail "Tag 版本 ($TAG_VERSION) 与 Cargo.toml 版本 ($CARGO_VERSION) 不匹配!" + else + pass "Tag 与 Cargo.toml 版本匹配" + fi +else + echo " (非 tag 构建,跳过版本匹配检查)" + pass "Cargo.toml 版本号格式正确: $CARGO_VERSION" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# 4. 检查 unsafe 代码 +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "--- 检查 unsafe 代码 ---" + +UNSAFE_HITS=$(grep -rn 'unsafe' src/ --include='*.rs' \ + | grep -v '#!\[forbid(unsafe_code)\]' \ + | grep -v '// unsafe' \ + | grep -v '#\[cfg(test)\]' \ + | grep -v 'mod tests' \ + || true) + +if [ -n "$UNSAFE_HITS" ]; then + fail "发现 unsafe 代码(本项目使用 #![forbid(unsafe_code)]):" + echo "$UNSAFE_HITS" | head -5 +else + pass "未发现 unsafe 代码" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# 汇总 +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "==========================================" +if [ "$ERRORS" -gt 0 ]; then + echo -e " ${RED}发现 $ERRORS 个错��!${NC}" + echo "==========================================" + exit 1 +else + echo -e " ${GREEN}所有安全检查通过${NC}" + echo "==========================================" + exit 0 +fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dba55c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,273 @@ +# ============================================================================= +# libsmx CI/CD 工作流 +# ============================================================================= +# 触发条件: +# - main 分支的 push / PR → lint + test + coverage +# - v* tag 推送 → 上述全部 + 自动发布 +# - PR → 额外运行 benchmark 并评论结果 +# ============================================================================= + +name: CI + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + +# 同一 PR 的新提交自动取消旧的运行,节省 CI 资源 +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + # 强制使用 sparse 协议,加速 crates.io 索引同步 + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + +# ============================================================================= +# Job 1: 代码质量检查(格式化 + Clippy + 依赖漏洞扫描) +# ============================================================================= +jobs: + lint: + name: Lint & Audit + runs-on: ubuntu-latest + steps: + - name: 签出代码 + uses: actions/checkout@v4 + + - name: 安装 Rust 工具链(stable + rustfmt + clippy) + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: 缓存 Cargo 编译产物 + uses: Swatinem/rust-cache@v2 + + - name: 检查代码格式 + run: cargo fmt --check + + - name: Clippy 检查(默认 features,所有警告视为错误) + run: cargo clippy --all-targets -- -D warnings + + - name: Clippy 检查(no_std,无 alloc) + # Reason: alloc-gated 函数在 no_std 模式下会产生 "unused" 假阳性, + # 因此只做编译检查,不启用 -D warnings + run: cargo check --no-default-features + + - name: 安装 cargo-audit + run: cargo install cargo-audit --locked + + - name: 依赖漏洞扫描(CVE 检查) + run: cargo audit + + - name: 安全性扫描(panic/unwrap 检查) + run: bash .github/scripts/sanity_check.sh + shell: bash + + # ============================================================================= + # Job 2: 多平台测试矩阵 + # ============================================================================= + test-matrix: + name: Test (${{ matrix.os }}) + needs: lint + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: 签出代码 + uses: actions/checkout@v4 + + - name: 安装 Rust 工具链(stable) + uses: dtolnay/rust-toolchain@stable + + - name: 缓存 Cargo 编译产物 + uses: Swatinem/rust-cache@v2 + + # Reason: 在所有平台上显式设置 bash 以统一 Windows/Linux/macOS 行为 + - name: 运行测试(默认 features = alloc) + run: cargo test --all-targets + shell: bash + + - name: 运行测试(no_std,无 alloc) + # Reason: 验证嵌入式 / WASM / 裸机兼容性 + run: cargo test --no-default-features --lib + shell: bash + + - name: 运行文档测试 + run: cargo test --doc + shell: bash + + - name: 构建文档(警告视为错误) + run: cargo doc --no-deps + env: + RUSTDOCFLAGS: "-D warnings" + shell: bash + + # ============================================================================= + # Job 3: MSRV 验证(最低支持版本) + # ============================================================================= + msrv: + name: MSRV (1.72.0) + needs: lint + runs-on: ubuntu-latest + steps: + - name: 签出代码 + uses: actions/checkout@v4 + + - name: 安装 MSRV 工具链 + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.72.0" + + - name: 缓存 Cargo 编译产物 + uses: Swatinem/rust-cache@v2 + + # Reason: MSRV 只验证能编译,不运行测试(避免 dev-dependencies 兼容问题) + - name: 检查编译(默认 features) + run: cargo check + + - name: 检查编译(no_std) + run: cargo check --no-default-features + + # ============================================================================= + # Job 4: 测试覆盖率(仅 Linux,上传 Codecov) + # ============================================================================= + coverage: + name: Coverage + needs: lint + runs-on: ubuntu-latest + steps: + - name: 签出代码 + uses: actions/checkout@v4 + + - name: 安装 Rust 工具链(stable) + uses: dtolnay/rust-toolchain@stable + + - name: 缓存 Cargo 编译产物 + uses: Swatinem/rust-cache@v2 + + - name: 安装 cargo-tarpaulin + run: cargo install cargo-tarpaulin --locked + + - name: 生成覆盖率报告 + run: cargo tarpaulin --out xml --output-dir coverage/ + + - name: 上传覆盖率到 Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage/cobertura.xml + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + # Reason: fail_ci_if_error=false 避免 Codecov 暂时不可用时阻断 CI + + # ============================================================================= + # Job 5: 性能回归检测(仅 PR 触发) + # ============================================================================= + benchmark: + name: Benchmark + if: github.event_name == 'pull_request' + needs: lint + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: 签出 PR 代码 + uses: actions/checkout@v4 + + - name: 安装 Rust 工具链(stable) + uses: dtolnay/rust-toolchain@stable + + - name: 缓存 Cargo 编译产物 + uses: Swatinem/rust-cache@v2 + + # Reason: 先运行当前 PR 分支的 benchmark 并保存结果 + - name: 运行 Benchmark + run: cargo bench --bench sm3_bench --bench sm4_bench --bench sm2_bench -- --output-format bencher 2>&1 | tee bench_output.txt + shell: bash + + # Reason: 将 benchmark 结果作为 PR 评论发布,方便审查性能变化 + - name: 发布 Benchmark 结果到 PR + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const benchOutput = fs.readFileSync('bench_output.txt', 'utf8'); + const body = `## Benchmark Results\n\n
\nClick to expand\n\n\`\`\`\n${benchOutput}\n\`\`\`\n\n
\n\n> Benchmarks ran on \`ubuntu-latest\`. Results may vary across runs.`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + + # ============================================================================= + # Job 6: 自动发布到 crates.io(仅 v* Tag 触发) + # ============================================================================= + release: + name: Release + if: startsWith(github.ref, 'refs/tags/v') + needs: [test-matrix, msrv] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: 签出代码 + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 安装 Rust 工具链(stable) + uses: dtolnay/rust-toolchain@stable + + - name: 缓存 Cargo 编译产物 + uses: Swatinem/rust-cache@v2 + + # Reason: 确保 Git Tag 版本号与 Cargo.toml 中的 version 字段完全一致 + - name: 验证 Tag 与 Cargo.toml 版本匹配 + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') + if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)" + exit 1 + fi + echo "Version: $TAG_VERSION ✓" + shell: bash + + # Reason: 发布前最后一轮完整测试,防止 tag 推送后才发现问题 + - name: 发布前完整测试 + run: cargo test --all-targets + + - name: 发布到 crates.io + run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + # Reason: 从 CHANGELOG.md 中提取当前版本的发布说明 + - name: 提取 Changelog 条目 + id: changelog + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + # 提取 ## [x.y.z] 到下一个 ## 之间的内容 + NOTES=$(awk "/^## \\[${TAG_VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md) + if [ -z "$NOTES" ]; then + NOTES="Release v${TAG_VERSION}" + fi + # 写入多行输出 + echo "notes<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + shell: bash + + - name: 创建 GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: "v${{ github.ref_name }}" + body: ${{ steps.changelog.outputs.notes }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} + # Reason: 如果 tag 包含 '-'(如 v0.4.0-rc1),标记为预发布 diff --git a/README.md b/README.md index 3de1fd8..eee3b9d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # libsmx +[![CI](https://github.com/kintaiW/libsmx/actions/workflows/ci.yml/badge.svg)](https://github.com/kintaiW/libsmx/actions/workflows/ci.yml) [![Crates.io](https://img.shields.io/crates/v/libsmx.svg)](https://crates.io/crates/libsmx) [![docs.rs](https://img.shields.io/docsrs/libsmx)](https://docs.rs/libsmx) +[![codecov](https://codecov.io/gh/kintaiW/libsmx/graph/badge.svg)](https://codecov.io/gh/kintaiW/libsmx) [![License](https://img.shields.io/crates/l/libsmx.svg)](LICENSE) [![MSRV](https://img.shields.io/badge/MSRV-1.72.0-blue.svg)](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) diff --git a/README.zh-CN.md b/README.zh-CN.md index da85e58..d223379 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,7 +1,9 @@ # libsmx +[![CI](https://github.com/kintaiW/libsmx/actions/workflows/ci.yml/badge.svg)](https://github.com/kintaiW/libsmx/actions/workflows/ci.yml) [![Crates.io](https://img.shields.io/crates/v/libsmx.svg)](https://crates.io/crates/libsmx) [![docs.rs](https://img.shields.io/docsrs/libsmx)](https://docs.rs/libsmx) +[![codecov](https://codecov.io/gh/kintaiW/libsmx/graph/badge.svg)](https://codecov.io/gh/kintaiW/libsmx) [![License](https://img.shields.io/crates/l/libsmx.svg)](LICENSE) [![MSRV](https://img.shields.io/badge/MSRV-1.72.0-blue.svg)](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) diff --git a/SECURITY.md b/SECURITY.md index 69eea71..8f316b9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,9 +20,8 @@ If you discover a security vulnerability in libsmx, please report it responsibly - Any potential impact assessment **Response timeline**: -- Acknowledgment within **48 hours** -- Initial assessment within **7 days** -- Fix release within **30 days** for confirmed issues + +本项目由个人维护,响应时间不设固定期限。作者会尽快处理所有安全报告,但无法承诺具体的响应时间。 ## Scope