Skip to content

Commit 15194b8

Browse files
authored
fix: harden module lifecycle bootstrap and signing workflows (#299)
* fix: harden module lifecycle bootstrap and signing workflows * fix: stabilize module signature hashing across environments * fix: stabilize bundle module signature verification in CI --------- Co-authored-by: Dominikus Nold <djm81@users.noreply.github.com>
1 parent 991d568 commit 15194b8

File tree

79 files changed

+3896
-1049
lines changed

Some content is hidden

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

79 files changed

+3896
-1049
lines changed

.github/workflows/pr-orchestrator.yml

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ jobs:
3838
- uses: actions/checkout@v4
3939
with:
4040
fetch-depth: 0
41+
with:
42+
fetch-depth: 0
4143
- uses: dorny/paths-filter@v3
4244
id: filter
4345
with:
@@ -64,9 +66,42 @@ jobs:
6466
echo "skip_tests_dev_to_main=false" >> "$GITHUB_OUTPUT"
6567
fi
6668
69+
verify-module-signatures:
70+
name: Verify Module Signatures
71+
needs: [changes]
72+
if: needs.changes.outputs.code_changed == 'true'
73+
runs-on: ubuntu-latest
74+
permissions:
75+
contents: read
76+
steps:
77+
- uses: actions/checkout@v4
78+
79+
- name: Set up Python 3.12
80+
uses: actions/setup-python@v5
81+
with:
82+
python-version: "3.12"
83+
cache: "pip"
84+
85+
- name: Install verifier dependencies
86+
run: |
87+
python -m pip install --upgrade pip
88+
python -m pip install pyyaml cryptography cffi
89+
90+
- name: Verify bundled module checksums and signatures
91+
run: |
92+
BASE_REF=""
93+
if [ "${{ github.event_name }}" = "pull_request" ]; then
94+
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
95+
fi
96+
if [ -n "$BASE_REF" ]; then
97+
python scripts/verify-modules-signature.py --require-signature --enforce-version-bump --version-check-base "$BASE_REF"
98+
else
99+
python scripts/verify-modules-signature.py --require-signature --enforce-version-bump
100+
fi
101+
67102
tests:
68103
name: Tests (Python 3.12)
69-
needs: [changes]
104+
needs: [changes, verify-module-signatures]
70105
if: needs.changes.outputs.code_changed == 'true'
71106
outputs:
72107
run_unit_coverage: ${{ steps.detect-unit.outputs.run_unit_coverage }}
@@ -583,6 +618,29 @@ jobs:
583618
run: |
584619
chmod +x .github/workflows/scripts/generate-release-notes.sh
585620
chmod +x .github/workflows/scripts/create-github-release.sh
621+
chmod +x scripts/sign-module.sh
622+
623+
- name: Sign bundled module manifests (release hardening)
624+
env:
625+
SPECFACT_MODULE_PRIVATE_SIGN_KEY: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY }}
626+
SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE }}
627+
run: |
628+
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY}" ]; then
629+
echo "❌ Missing required secret: SPECFACT_MODULE_PRIVATE_SIGN_KEY"
630+
exit 1
631+
fi
632+
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE}" ]; then
633+
echo "❌ Missing required secret: SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE"
634+
exit 1
635+
fi
636+
python -m pip install --upgrade pip
637+
python -m pip install pyyaml cryptography cffi
638+
mapfile -t MANIFESTS < <(find src/specfact_cli/modules -name 'module-package.yaml' -type f)
639+
if [ "${#MANIFESTS[@]}" -eq 0 ]; then
640+
echo "No bundled module manifests found to sign."
641+
exit 0
642+
fi
643+
python scripts/sign-modules.py "${MANIFESTS[@]}"
586644
587645
- name: Get version from PyPI publish step
588646
id: get_version

.github/workflows/sign-modules.yml

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,102 @@
11
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2-
# Sign module manifests for integrity (arch-06). Outputs checksums for manifest integrity fields.
3-
name: Sign Modules
2+
# Harden module signing by enforcing strict verification and deterministic signing output checks.
3+
name: Module Signature Hardening
44

55
on:
66
workflow_dispatch: {}
77
push:
8-
branches: [main]
8+
branches: [dev, main]
99
paths:
10-
- "src/specfact_cli/modules/**/module-package.yaml"
11-
- "modules/**/module-package.yaml"
10+
- "src/specfact_cli/modules/**"
11+
- "modules/**"
12+
- "resources/keys/**"
13+
- "scripts/sign-modules.py"
14+
- "scripts/verify-modules-signature.py"
15+
- ".github/workflows/sign-modules.yml"
16+
pull_request:
17+
branches: [dev, main]
18+
paths:
19+
- "src/specfact_cli/modules/**"
20+
- "modules/**"
21+
- "resources/keys/**"
22+
- "scripts/sign-modules.py"
23+
- "scripts/verify-modules-signature.py"
24+
- ".github/workflows/sign-modules.yml"
1225

1326
jobs:
14-
sign:
15-
name: Sign module manifests
27+
verify:
28+
name: Verify module signatures
1629
runs-on: ubuntu-latest
1730
permissions:
1831
contents: read
1932
steps:
2033
- name: Checkout repository
2134
uses: actions/checkout@v4
35+
with:
36+
fetch-depth: 0
37+
38+
- name: Set up Python
39+
uses: actions/setup-python@v5
40+
with:
41+
python-version: "3.12"
2242

23-
- name: Sign module manifests
43+
- name: Install signer dependencies
2444
run: |
25-
for f in $(find . -name 'module-package.yaml' -not -path './.git/*' 2>/dev/null | head -20); do
26-
if [ -f "scripts/sign-module.sh" ]; then
27-
bash scripts/sign-module.sh "$f" || true
28-
fi
29-
done
45+
python -m pip install --upgrade pip
46+
python -m pip install pyyaml cryptography cffi
47+
48+
- name: Verify bundled module signatures
49+
run: |
50+
BASE_REF=""
51+
if [ "${{ github.event_name }}" = "pull_request" ]; then
52+
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
53+
fi
54+
if [ -n "$BASE_REF" ]; then
55+
python scripts/verify-modules-signature.py --require-signature --enforce-version-bump --version-check-base "$BASE_REF"
56+
else
57+
python scripts/verify-modules-signature.py --require-signature --enforce-version-bump
58+
fi
59+
60+
reproducibility:
61+
name: Assert signing reproducibility
62+
runs-on: ubuntu-latest
63+
needs: [verify]
64+
permissions:
65+
contents: read
66+
steps:
67+
- name: Checkout repository
68+
uses: actions/checkout@v4
69+
70+
- name: Set up Python
71+
uses: actions/setup-python@v5
72+
with:
73+
python-version: "3.12"
74+
75+
- name: Install signer dependencies
76+
run: |
77+
python -m pip install --upgrade pip
78+
python -m pip install pyyaml cryptography cffi
79+
80+
- name: Re-sign manifests and assert no diff
81+
env:
82+
SPECFACT_MODULE_PRIVATE_SIGN_KEY: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY }}
83+
SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE }}
84+
run: |
85+
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY}" ]; then
86+
echo "::notice::Skipping reproducibility check because SPECFACT_MODULE_PRIVATE_SIGN_KEY is not configured."
87+
exit 0
88+
fi
89+
90+
mapfile -t MANIFESTS < <(find src/specfact_cli/modules modules -name 'module-package.yaml' -type f 2>/dev/null | sort)
91+
if [ "${#MANIFESTS[@]}" -eq 0 ]; then
92+
echo "No module manifests found"
93+
exit 0
94+
fi
95+
96+
python scripts/sign-modules.py "${MANIFESTS[@]}"
97+
98+
if ! git diff --exit-code -- src/specfact_cli/modules modules; then
99+
echo "::error::Module signatures are stale for the configured signing key. Re-sign and commit manifest updates."
100+
git --no-pager diff --name-only -- src/specfact_cli/modules modules
101+
exit 1
102+
fi

AGENTS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,25 @@ Run all steps in order before committing. Every step must pass with no errors.
165165
5. `hatch run contract-test` # contract-first validation
166166
6. `hatch run smart-test` # targeted test run (use `smart-test-full` for larger modifications)
167167

168+
### Module Signature Gate (Required for Change Finalization)
169+
170+
Before PR creation, every change MUST pass bundled module signature verification:
171+
172+
1. Run `hatch run ./scripts/verify-modules-signature.py --require-signature`.
173+
2. If verification fails because module contents changed, re-sign affected manifests:
174+
- `hatch run python scripts/sign-modules.py --key-file <private-key.pem> <module-package.yaml ...>`
175+
3. Re-run verification until green.
176+
177+
Rules:
178+
179+
- Do not merge/PR with stale or missing integrity metadata for bundled modules.
180+
- Treat signature verification as a quality gate equal to lint/type-check/tests.
181+
- Module version bump is mandatory before signing changed module contents. Do not keep the same module version when module files or signatures change.
182+
- For any module re-sign/sign operation, increment module version using semver (major/minor/patch) so published/registered versions are immutable.
183+
- Use signer/verifier enforcement paths:
184+
- signer rejects changed modules with unchanged version by default;
185+
- verifier/CI enforces version-bump checks for changed manifests.
186+
168187
### OpenSpec Workflow
169188

170189
Before modifying application code, **always** verify that an active OpenSpec change in `openspec/changes/` **explicitly covers the requested modification**. This is the spec-driven workflow defined in `openspec/config.yaml`. Skip only when the user explicitly says `"skip openspec"` or `"implement without openspec change"`.

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,38 @@ All notable changes to this project will be documented in this file.
88
**Important:** Changes need to be documented below this block as this is the header section. Each section should be separated by a horizontal rule. Newer changelog entries need to be added on top of prior ones to keep the history chronological with most recent changes first.
99

1010

11+
---
12+
13+
## [0.37.0] - 2026-02-23
14+
15+
### Added
16+
17+
- Bundled module signing/verification now covers full module payload contents (all files in module directory), not only manifest fields.
18+
- `scripts/sign-module.sh` / `scripts/sign-modules.py` now support encrypted private keys with passphrase input via `--passphrase`, `--passphrase-stdin`, or `SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE`.
19+
- CI signing/verification workflow wiring now uses dedicated secrets `SPECFACT_MODULE_PRIVATE_SIGN_KEY` and `SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE`.
20+
- Signature verification tooling now supports module-version policy checks (`--enforce-version-bump`, `--version-check-base`) to prevent re-signing changed contents under unchanged versions.
21+
22+
### Changed
23+
24+
- `specfact init` output now explicitly points users to `specfact module` for module lifecycle commands.
25+
- `specfact module install` / `uninstall` now support explicit scope targeting (`user` or `project`) with `--repo` for project scope.
26+
- `specfact module install` command/help now documents and supports bundled-source resolution controls so users can install shipped modules selectively through the same lifecycle flow as marketplace installs.
27+
28+
### Fixed
29+
30+
- `specfact init` now seeds shipped module artifacts into `~/.specfact/modules`, so commands contributed by shipped modules (for example `specfact backlog add`) no longer depend on repository-local `modules/` folders.
31+
- Module installer/discovery now recognizes `~/.specfact/modules` as a canonical per-user root while remaining backward-compatible with legacy module roots.
32+
- Workspace-local module discovery is now restricted to `<repo>/.specfact/modules` (not `<repo>/modules`), preventing accidental ownership of arbitrary repository folders.
33+
- In repository context, project modules from `<repo>/.specfact/modules` now take precedence over user modules from `~/.specfact/modules`.
34+
- Added `specfact module init --scope project [--repo PATH]` so bundled modules can be seeded per-project, while default `specfact module init` continues to seed user scope.
35+
- Startup checks now include bundled-module freshness guidance on CLI version change and at most once per 24 hours, with actionable commands for project and user scopes.
36+
- Removed deprecated `specfact init` lifecycle flags (`--list-modules`, `--enable-module`, `--disable-module`) so module lifecycle management lives only under `specfact module`.
37+
- Added `specfact module list --show-bundled-available` to display bundled modules that are available locally but not yet installed, with user/project scope install hints.
38+
- `specfact module install` now resolves bundled modules before marketplace fallback, enabling subset install of shipped bundles.
39+
- `specfact module uninstall` now blocks ambiguous removals when module IDs exist in both user and project roots unless `--scope` is explicitly selected.
40+
- Module integrity runtime checks now avoid transient runtime artifacts (for example Python cache files) so installed modules do not fail trust checks due to local generated files.
41+
- Uninstall now correctly resolves legacy marketplace install roots when applicable, preventing false-success uninstall outcomes during upgrades.
42+
1143
---
1244

1345
## [0.36.1] - 2026-02-23
@@ -111,6 +143,8 @@ All notable changes to this project will be documented in this file.
111143
- `docs/index.md`
112144
- Simplified top-level `README.md` by removing deep architecture implementation details and linking technical readers to architecture docs.
113145

146+
### Fixed
147+
114148
---
115149

116150
## [0.33.0] - 2026-02-17

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ SpecFact CLI uses a lifecycle-managed module system:
8181
- `specfact init` bootstraps local state.
8282
- `specfact init ide` handles IDE prompt/template installation and updates.
8383
- `specfact module` is the canonical lifecycle surface for install/list/show/search/enable/disable/uninstall/upgrade.
84-
- `specfact init --list-modules`, `--enable-module`, and `--disable-module` remain compatibility aliases.
8584
- Dependency and compatibility guards prevent invalid module states; `--force` enables dependency-aware cascades.
8685

8786
This is the baseline for marketplace-driven module lifecycle and future community module distribution.
@@ -106,6 +105,7 @@ For implementation details, see:
106105
- [Module Contracts](reference/module-contracts.md)
107106
- [Installing Modules](guides/installing-modules.md)
108107
- [Module Marketplace](guides/module-marketplace.md)
108+
- [Module Signing and Key Rotation](guides/module-signing-and-key-rotation.md)
109109

110110
---
111111

docs/_layouts/default.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ <h2 class="docs-sidebar-title">
133133
<ul>
134134
<li><a href="{{ '/getting-started/installation/' | relative_url }}">Installation</a></li>
135135
<li><a href="{{ '/getting-started/first-steps/' | relative_url }}">First Steps</a></li>
136+
<li><a href="{{ '/getting-started/module-bootstrap-checklist/' | relative_url }}">Module Bootstrap Checklist</a></li>
136137
<li><a href="{{ '/getting-started/tutorial-backlog-refine-ai-ide/' | relative_url }}">Tutorial: Backlog Refine with AI IDE</a></li>
137138
<li><a href="{{ '/getting-started/tutorial-daily-standup-sprint-review/' | relative_url }}">Tutorial: Daily Standup and Sprint Review</a></li>
138139
</ul>
@@ -148,6 +149,7 @@ <h2 class="docs-sidebar-title">
148149
<li><a href="{{ '/guides/extending-projectbundle/' | relative_url }}">Extending ProjectBundle</a></li>
149150
<li><a href="{{ '/guides/installing-modules/' | relative_url }}">Installing Modules</a></li>
150151
<li><a href="{{ '/guides/module-marketplace/' | relative_url }}">Module Marketplace</a></li>
152+
<li><a href="{{ '/guides/module-signing-and-key-rotation/' | relative_url }}">Module Signing and Key Rotation</a></li>
151153
<li><a href="{{ '/guides/using-module-security-and-extensions/' | relative_url }}">Using Module Security and Extensions</a></li>
152154
<li><a href="{{ '/brownfield-engineer/' | relative_url }}">Working With Existing Code</a></li>
153155
<li><a href="{{ '/brownfield-journey/' | relative_url }}">Existing Code Journey</a></li>

docs/getting-started/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ uvx specfact-cli@latest plan init my-project --interactive
5151

5252
- 📖 **[Installation Guide](installation.md)** - Install SpecFact CLI
5353
- 📖 **[First Steps](first-steps.md)** - Step-by-step first commands
54+
- 📖 **[Module Bootstrap Checklist](module-bootstrap-checklist.md)** - Verify bundled modules are installed in user/project scope
5455
- 📖 **[Tutorial: Using SpecFact with OpenSpec or Spec-Kit](tutorial-openspec-speckit.md)****NEW** - Complete beginner-friendly tutorial
5556
- 📖 **[DevOps Backlog Integration](../guides/devops-adapter-integration.md)** 🆕 **NEW FEATURE** - Integrate SpecFact into agile DevOps workflows
5657
- 📖 **[Backlog Refinement](../guides/backlog-refinement.md)** 🆕 **NEW FEATURE** - AI-assisted template-driven refinement for standardizing work items

0 commit comments

Comments
 (0)