From b58b6358f4fcd1592809fb818968f29f1d46b397 Mon Sep 17 00:00:00 2001 From: MrSphay Date: Fri, 15 May 2026 23:47:10 +0200 Subject: [PATCH] Add MrTrust GUI and Gitea release build --- .gitea/workflows/build.yml | 49 +++ .gitignore | 9 + AGENTS.md | 71 ++-- CHANGELOG.md | 44 +- MrTrust.ps1 | 52 +++ README.md | 389 ++++-------------- assets/certificates/MrSphay-CodeSigning.cer | Bin 0 -> 1335 bytes .../certificates/MrSphay-LocalTrust-Root.cer | Bin 0 -> 1305 bytes assets/certificates/README.md | 15 + assets/certificates/thumbprints.txt | 7 + docs/integration-prompt.md | 33 ++ docs/security-model.md | 40 ++ scripts/Build-MrTrustExe.ps1 | 50 +++ scripts/Install-MrTrust.ps1 | 90 ++++ scripts/New-MrTrustCertificate.ps1 | 90 ++++ scripts/New-MrTrustRelease.ps1 | 70 ++++ scripts/Sign-MrTrustProject.ps1 | 104 +++++ scripts/Start-MrTrustGui.ps1 | 324 +++++++++++++++ scripts/Uninstall-MrTrust.ps1 | 87 ++++ src/MrTrustLauncher.cs | 58 +++ 20 files changed, 1179 insertions(+), 403 deletions(-) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitignore create mode 100644 MrTrust.ps1 create mode 100644 assets/certificates/MrSphay-CodeSigning.cer create mode 100644 assets/certificates/MrSphay-LocalTrust-Root.cer create mode 100644 assets/certificates/README.md create mode 100644 assets/certificates/thumbprints.txt create mode 100644 docs/integration-prompt.md create mode 100644 docs/security-model.md create mode 100644 scripts/Build-MrTrustExe.ps1 create mode 100644 scripts/Install-MrTrust.ps1 create mode 100644 scripts/New-MrTrustCertificate.ps1 create mode 100644 scripts/New-MrTrustRelease.ps1 create mode 100644 scripts/Sign-MrTrustProject.ps1 create mode 100644 scripts/Start-MrTrustGui.ps1 create mode 100644 scripts/Uninstall-MrTrust.ps1 create mode 100644 src/MrTrustLauncher.cs diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..f69b785 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,49 @@ +name: Build MrTrust + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + build-windows: + runs-on: windows + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify PowerShell scripts + shell: powershell + run: | + $scripts = @() + $scripts += @(Get-ChildItem . -Filter *.ps1) + $scripts += @(Get-ChildItem .\scripts -Filter *.ps1) + foreach ($script in $scripts) { + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($script.FullName, [ref]$tokens, [ref]$errors) | Out-Null + if ($errors) { throw $errors } + } + + - name: Build release ZIP + shell: powershell + run: | + $arguments = @("-ExecutionPolicy", "Bypass", "-File", ".\scripts\New-MrTrustRelease.ps1", "-Version", "0.1.0") + if ($env:MRTRUST_SIGNING_THUMBPRINT) { + $arguments += @("-SigningThumbprint", $env:MRTRUST_SIGNING_THUMBPRINT) + } + powershell @arguments + + - name: Show package contents + shell: powershell + run: | + Get-ChildItem .\dist -Recurse | Select-Object FullName, Length + + - name: Upload release artifact + uses: actions/upload-artifact@v3 + with: + name: MrTrust-0.1.0 + path: dist/MrTrust-0.1.0.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f46df26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +private/ +dist/ +*.pfx +*.snk +*.log +bin/ +obj/ +.vs/ +.vscode/ diff --git a/AGENTS.md b/AGENTS.md index 6dad8bc..4f062ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,60 +1,35 @@ -# Agent Instructions For This Repository +# Agent Instructions For MrTrust -This file is for Codex agents working on the Codex Agent Repository Kit itself. The public `README.md` is for humans and should stay focused on setup and usage. +MrTrust manages explicit Windows certificate trust for MrSphay software. -## Start Of Task +## Security Boundaries -- Check `git status --short`. -- If the working tree is clean, run `git pull --ff-only` before editing. -- If local changes exist, preserve them and do not overwrite user work. -- Conserve context tokens: use `rg`, targeted file reads, and short summaries instead of loading unrelated files or long logs. +- Do not add Defender, SmartScreen, UAC, firewall, or policy bypasses. +- Do not add silent certificate installation. +- Do not commit `.pfx`, private keys, passwords, tokens, or signing secrets. +- Default to `CurrentUser` certificate stores. Use `LocalMachine` only when the user explicitly chooses all-user trust. +- Keep all user-facing trust actions reversible. -## Repository Purpose +## Repository Layout -This repository ships reusable baseline files for other repositories: - -- `files/` contains templates copied into target repositories. -- `agent-quickstart.md`, `new-repository.md`, and `existing-project.md` are agent workflows. -- `manifest.json` is the source of truth for copy targets and placeholders. -- `profiles/` contains stack-specific guidance. - -## Editing Rules - -- Keep repository owner, repository name, project names, and local paths dynamic. This kit intentionally targets `https://git.wilkensxl.de` and SSH port `2222`, so keep that host/port consistent in user-facing setup and Gitea workflow defaults. -- If a new placeholder is introduced, update `manifest.json`, the README placeholder list, and placeholder scans in workflow templates. -- Keep `README.md` user-facing. Put agent operating rules in this file or the workflow docs. -- Keep `files/AGENTS.md` generic; it is copied into target repositories and must not describe this repository specifically. -- Do not include secrets, tokens, private data, or sensitive logs in docs, issues, commits, or release notes. - -## Follow-up Work - -- Create focused tracker issues for real follow-up work that is outside the current scope or can be done independently. -- Do not create issues for work that can be safely completed in the current task. -- If issue creation is unavailable, update `docs/agent-handoff.md` with the blocker and next steps. +- `scripts/` contains the PowerShell implementation. +- `assets/certificates/` contains public certificates only. +- `private/` is ignored and may contain local signing material. +- `docs/integration-prompt.md` is the prompt for adding MrTrust to other projects. +- `docs/security-model.md` documents the intended behavior and limits. +- `MrTrust.ps1 gui` is the user-facing GUI entry point. ## Verification -Before committing: +Before finishing changes, run: ```powershell -Get-Content manifest.json | ConvertFrom-Json | Out-Null -Get-Content manifest.schema.json | ConvertFrom-Json | Out-Null -Get-Content files\blueprint.json | ConvertFrom-Json | Out-Null +$scripts = Get-ChildItem .\scripts -Filter *.ps1 +foreach ($script in $scripts) { + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($script.FullName, [ref]$tokens, [ref]$errors) | Out-Null + if ($errors) { throw $errors } +} git diff --check ``` - -Also verify: - -- every `manifest.json` copyMap source exists, -- every profile path exists, -- reusable files contain no private instance defaults such as a specific username or private host, -- `README.md` documents every placeholder listed in `manifest.json`. - -## Release - -- Bump `manifest.json` version. -- Update `CHANGELOG.md`. -- Commit changes. -- Create an annotated tag such as `v1.0.2`. -- Push `main` and tags. -- Create or update the Gitea release when a valid API token is available. diff --git a/CHANGELOG.md b/CHANGELOG.md index a408b99..f79b806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,41 +1,9 @@ # Changelog -All notable changes to the Codex Agent Repository Kit are documented here. +## 0.1.0 -## 1.0.5 - 2026-05-15 - -- Restored the rainbow section divider theme in the human-facing `README.md`. -- Added separate minimal permission guidance for `REGISTRY_TOKEN` and `GITEA_TOKEN`. -- Clarified where package-only and API-capable tokens should be used. - -## 1.0.4 - 2026-05-15 - -- Set the documented Gitea host to `git.wilkensxl.de` instead of a generic URL placeholder. -- Documented SSH clone URLs for port `2222` and optional SSH config. -- Restored Gitea workflow and README badge defaults for the intended Gitea instance while keeping repository owner and repository name dynamic. - -## 1.0.3 - 2026-05-15 - -- Updated repository handoff notes after verifying the refreshed local `GITEA_TOKEN`. -- Confirmed live issue creation and Gitea release API access for this repository. - -## 1.0.2 - 2026-05-15 - -- Split the repository documentation into a human-facing setup `README.md` and a repository-specific agent instruction file in `AGENTS.md`. -- Expanded the human README with full new-repository setup guidance, SSH setup, Gitea token permissions, local token configuration, repository secrets, package publishing, and release checks. -- Documented the recommended Gitea token permission matrix shown in the token UI. - -## 1.0.1 - 2026-05-15 - -- Added agent guidance to create focused tracker issues for actionable follow-up work that is outside the current scope or independently parallelizable. -- Added safeguards against creating vague, duplicate, or sensitive public issues. -- Updated handoff guidance to use `docs/agent-handoff.md` when no issue tracker is available or the details are too sensitive for public issues. - -## 1.0.0 - 2026-05-15 - -- Added universal repository baseline templates for Codex-assisted projects. -- Added agent quickstart, new repository, and existing project workflows. -- Added optional Gitea workflow templates for build, security scanning, cleanup, dependency checks, release dry runs, and template compliance. -- Added stack profiles for Node, Electron, Python, Docker, and static sites. -- Added guidance for dynamic repository owners, safe task-start syncs, release artifact exclusions, and context token conservation. -- Removed hard-coded private Gitea instance URLs from reusable templates. +- Added MrTrust certificate generation, installation, removal, and signing scripts. +- Added a simple Windows GUI for installing and removing MrTrust. +- Added a Windows launcher EXE and Gitea Runner workflow for release ZIP builds. +- Added integration prompt for other Windows projects. +- Added security model documentation. diff --git a/MrTrust.ps1 b/MrTrust.ps1 new file mode 100644 index 0000000..5bfd2af --- /dev/null +++ b/MrTrust.ps1 @@ -0,0 +1,52 @@ +param( + [Parameter(Position = 0)] + [ValidateSet("gui", "install", "uninstall", "new-cert", "sign", "help")] + [string]$Command = "help", + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArguments +) + +$ErrorActionPreference = "Stop" + +$root = Split-Path -Parent $MyInvocation.MyCommand.Path + +function Invoke-MrTrustScript { + param([Parameter(Mandatory)][string]$ScriptPath) + + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $ScriptPath @RemainingArguments + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} + +switch ($Command) { + "gui" { + Invoke-MrTrustScript -ScriptPath (Join-Path $root "scripts\Start-MrTrustGui.ps1") + } + "install" { + Invoke-MrTrustScript -ScriptPath (Join-Path $root "scripts\Install-MrTrust.ps1") + } + "uninstall" { + Invoke-MrTrustScript -ScriptPath (Join-Path $root "scripts\Uninstall-MrTrust.ps1") + } + "new-cert" { + Invoke-MrTrustScript -ScriptPath (Join-Path $root "scripts\New-MrTrustCertificate.ps1") + } + "sign" { + Invoke-MrTrustScript -ScriptPath (Join-Path $root "scripts\Sign-MrTrustProject.ps1") + } + default { + Write-Host "MrTrust commands:" + Write-Host " .\MrTrust.ps1 gui Open the MrTrust Windows interface" + Write-Host " .\MrTrust.ps1 install Install MrSphay public trust certificates" + Write-Host " .\MrTrust.ps1 uninstall Remove MrSphay public trust certificates" + Write-Host " .\MrTrust.ps1 new-cert Create MrSphay root and signing certificates" + Write-Host " .\MrTrust.ps1 sign Sign build artifacts" + Write-Host "" + Write-Host "Examples:" + Write-Host " .\MrTrust.ps1 gui" + Write-Host " .\MrTrust.ps1 install" + Write-Host " .\MrTrust.ps1 install -Scope LocalMachine" + Write-Host " .\MrTrust.ps1 sign -Path .\dist\MyApp.exe -PfxPath .\private\MrSphay-CodeSigning.pfx" + } +} diff --git a/README.md b/README.md index 85b7cab..547b1e2 100644 --- a/README.md +++ b/README.md @@ -1,368 +1,123 @@ -# Codex Agent Repository Kit +# MrTrust -Reusable setup kit for new or existing repositories that should be easy for Codex agents, humans, and CI workflows to maintain. +MrTrust is a small Windows trust-onboarding kit for MrSphay software. -This README is for humans. Agent-facing rules live in `AGENTS.md`, `agent-quickstart.md`, `new-repository.md`, and `existing-project.md`. +It is designed for this workflow: -

-----------------------------------------------------

+1. MrSphay creates a private code-signing certificate once. +2. MrSphay publishes only the public trust certificate with MrTrust. +3. A user runs MrTrust once and explicitly approves installing that public certificate. +4. MrSphay projects signed with the matching certificate chain are shown as trusted on that PC. -## What This Kit Adds +MrTrust does not bypass Microsoft Defender or SmartScreen. Windows can still scan, quarantine, or warn about suspicious files. This project only manages normal Windows certificate trust with visible user consent. -- `AGENTS.md` and `.codex/project.md` for agent context. -- Optional Gitea workflows for build, security scan, cleanup, dependency check, release dry run, and template compliance. -- Release, security, handoff, changelog, and contribution templates. -- README blueprint templates for projects that want generated README output. -- Stack notes for Node, Electron, Python, Docker, and static-site projects. +## What It Contains -

-----------------------------------------------------

+- `MrTrust.ps1 gui` opens a simple Windows interface for installing or removing trust. +- `scripts/New-MrTrustCertificate.ps1` creates a local root certificate and a code-signing certificate for the publisher. +- `scripts/Install-MrTrust.ps1` installs the public trust certificate for the current user or the local machine. +- `scripts/Uninstall-MrTrust.ps1` removes the MrTrust certificate again. +- `scripts/Sign-MrTrustProject.ps1` signs `.exe`, `.msi`, `.ps1`, and other Authenticode-compatible files. +- `scripts/New-MrTrustRelease.ps1` builds a distributable ZIP package. +- `docs/integration-prompt.md` is a prompt you can paste into other Windows projects. -## Recommended New Repository Setup +## Quick Start For MrSphay -1. Create the repository in Gitea. -2. Clone it locally with SSH. -3. Copy this kit into the repository with Codex or manually from `files/`. -4. Replace placeholders with real project values. -5. Add repository secrets for CI publishing. -6. Commit and push the baseline. -7. Let the Gitea workflows report any missing setup. - -

-----------------------------------------------------

- -## SSH Setup - -Generate a key if you do not already have one: +Create the certificates: ```powershell -ssh-keygen -t ed25519 -C "you@example.com" +.\scripts\New-MrTrustCertificate.ps1 ``` -Start the SSH agent and add the key: +This writes: + +- public certificates to `assets\certificates\` +- private signing material to `private\` + +The `private\` directory is ignored by git. Do not publish `.pfx` files or passwords. + +Install the public trust certificate on your own PC: ```powershell -Start-Service ssh-agent -ssh-add $env:USERPROFILE\.ssh\id_ed25519 +.\MrTrust.ps1 install ``` -Show the public key: +Open the GUI: ```powershell -Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub +.\MrTrust.ps1 gui ``` -Add that public key in Gitea: - -```text -Profile -> Settings -> SSH / GPG Keys -> Add Key -``` - -Clone with SSH: - -```bash -git clone ssh://git@git.wilkensxl.de:2222/OWNER/REPOSITORY.git -cd REPOSITORY -``` - -Optional SSH config: - -```text -Host git.wilkensxl.de - HostName git.wilkensxl.de - User git - Port 2222 - IdentityFile ~/.ssh/id_ed25519 -``` - -With that config, this shorter clone URL also works: - -```bash -git clone git@git.wilkensxl.de:OWNER/REPOSITORY.git -``` - -Verify the remote: - -```bash -git remote -v -git status --short -``` - -

-----------------------------------------------------

- -## Applying The Kit With Codex - -For a new repository, start Codex in the target repository and use: - -```text -Use the Codex Agent Repository Kit. -Read manifest.json, then use new-repository.md. -Create the smallest useful baseline for this repository. -Replace placeholders with real values from this repository. -Keep commands truthful and do not invent scripts that cannot run. -Do not create a release. -``` - -For an existing repository: - -```text -Use the Codex Agent Repository Kit. -Read manifest.json, then use existing-project.md. -Retrofit the baseline without replacing existing project structure or README knowledge. -Preserve current CI behavior and project style. -Do not create a release. -``` - -

-----------------------------------------------------

- -## Manual Copy Map - -Use `manifest.json` as the source of truth. Common targets: - -| Template | Target | -| --- | --- | -| `files/AGENTS.md` | `AGENTS.md` | -| `files/project.md` | `.codex/project.md` | -| `files/build-gitea.yml` | `.gitea/workflows/build.yml` | -| `files/security-scan-gitea.yml` | `.gitea/workflows/security-scan.yml` | -| `files/repo-cleanup-gitea.yml` | `.gitea/workflows/repo-cleanup.yml` | -| `files/dependency-check-gitea.yml` | `.gitea/workflows/dependency-check.yml` | -| `files/release-dry-run-gitea.yml` | `.gitea/workflows/release-dry-run.yml` | -| `files/template-compliance-gitea.yml` | `.gitea/workflows/template-compliance.yml` | -| `files/SECURITY.md` | `SECURITY.md` | -| `files/CHANGELOG.md` | `CHANGELOG.md` | -| `files/CONTRIBUTING.md` | `CONTRIBUTING.md` | -| `files/release-checklist.md` | `docs/release-checklist.md` | -| `files/security-review.md` | `docs/security-review.md` | -| `files/agent-handoff.md` | `docs/agent-handoff.md` | - -

-----------------------------------------------------

- -## Required Placeholder Values - -Replace or remove all placeholders before considering a repository ready: - -```text -PROJECT_NAME -PROJECT_DESCRIPTION -REPOSITORY_OWNER -REPOSITORY_NAME -PACKAGE_NAME -ARTIFACT_NAME -ARTIFACT_OUTPUT_DIRECTORY -AUTHOR_NAME -PROJECT_STACK -DOWNLOAD_URL -CI_URL -RELEASES_URL -BUILD_COMMAND -TEST_COMMAND -LINT_COMMAND -AUDIT_COMMAND -README_COMMAND -INSTALL_COMMAND -DEV_COMMAND -PACKAGE_MANAGER -PROJECT_VERSION -COMMIT_OR_VERSION -``` - -If a value does not apply, remove that section instead of leaving fake data. If a value is genuinely unknown, mark it as `PENDING`. - -

-----------------------------------------------------

- -## Token Overview - -Use separate tokens for separate jobs. - -| Token | Location | Purpose | -| --- | --- | --- | -| `REGISTRY_TOKEN` | Repository secret | CI package publishing from Gitea Actions | -| `GITEA_TOKEN` | Local environment or repository secret | Gitea API access for issues, releases, workflow polling, and repository metadata | - -Repository secrets are available to workflows. They are not visible to local Codex sessions. Local Codex API actions need a local environment variable. - -

-----------------------------------------------------

- -## Gitea Token Permissions - -For both tokens, choose this repository access level: - -```text -Repository and Organization Access: All (public, private, and limited) -``` - -Use separate tokens where possible. A package-only token should not be able to create issues or releases. - -### REGISTRY_TOKEN Permissions - -Use this token as a repository secret for package publishing from Gitea Actions: - -```text -package: Read and Write -repository: Read -user: Read - -activitypub: No Access -admin: No Access -issue: No Access -misc: No Access -notification: No Access -organization: No Access -``` - -These permissions cover generic package uploads while still allowing the workflow to read repository metadata. - -### GITEA_TOKEN Permissions - -Use this token locally on the PC for Codex API actions, or as a repository secret only when workflows need issue, release, or workflow API access: - -```text -issue: Read and Write -package: Read -repository: Read and Write -user: Read - -activitypub: No Access -admin: No Access -misc: No Access -notification: No Access -organization: No Access -``` - -These permissions cover creating and reading issues, creating and reading releases, reading repository metadata, and polling workflow runs where the Gitea API allows it. `package: Read` is enough for API checks; use `package: Read and Write` only if this same token must publish packages. - -Use a dedicated bot or automation user when possible. - -

-----------------------------------------------------

- -## Setting Local Tokens - -Set a local token for Codex or shell-based API work. - -Current PowerShell session: +Sign another project build: ```powershell -$env:GITEA_TOKEN = "paste-token-here" +.\MrTrust.ps1 sign ` + -Path "C:\Path\To\App.exe" ` + -PfxPath ".\private\MrSphay-CodeSigning.pfx" ``` -Persist for the current Windows user: +Remove the trust certificate: ```powershell -setx GITEA_TOKEN "paste-token-here" +.\MrTrust.ps1 uninstall ``` -Open a new terminal after `setx`. - -Test repository API access: +Build a user-facing ZIP release: ```powershell -$headers = @{ Authorization = "token $env:GITEA_TOKEN" } -Invoke-RestMethod ` - -Uri "https://git.wilkensxl.de/api/v1/repos/REPOSITORY_OWNER/REPOSITORY_NAME" ` - -Headers $headers +.\scripts\New-MrTrustRelease.ps1 -Version 0.1.0 ``` -Test issue access: +The Gitea workflow `.gitea/workflows/build.yml` builds the same ZIP on a Windows runner and uploads it as an artifact. +If the Windows runner has the private signing certificate installed, set `MRTRUST_SIGNING_THUMBPRINT` to sign the launcher during the build. + +## User Installation + +For normal users, distribute MrTrust with the public certificate file: + +```text +assets\certificates\MrSphay-LocalTrust-Root.cer +assets\certificates\MrSphay-CodeSigning.cer +``` + +The user runs: ```powershell -Invoke-RestMethod ` - -Uri "https://git.wilkensxl.de/api/v1/repos/REPOSITORY_OWNER/REPOSITORY_NAME/issues?state=open&limit=1" ` - -Headers $headers +.\MrTrust.ps1 gui ``` -

-----------------------------------------------------

- -## Setting Repository Secrets - -In Gitea: +By default, MrTrust installs trust only for the current Windows user: ```text -Repository -> Settings -> Actions -> Secrets -> Add Secret +Root certificate -> Cert:\CurrentUser\Root +Code-signing certificate -> Cert:\CurrentUser\TrustedPublisher ``` -Add: +For all users on the machine, run PowerShell as Administrator: -```text -REGISTRY_TOKEN +```powershell +.\MrTrust.ps1 install -Scope LocalMachine ``` -Use a token with package write access. If you want workflows to create releases or issues too, add a separate secret: +## Using This Repo With Other Agents -```text -GITEA_TOKEN -``` +Yes. Give another agent this repository URL and the target Windows project, then paste `docs/integration-prompt.md`. -Keep package publishing and release or issue automation separate when possible. It makes permission reviews easier. +Both sides have to be wired: -

-----------------------------------------------------

+- MrTrust side: users install the public trust certificates once. +- Target project side: release artifacts are signed with the MrSphay code-signing certificate. +- Installer side, optional: the target app can offer "Open MrTrust" or bundle the MrTrust ZIP, but it must not silently change trust. -## Package Publishing +If the target project is not signed, MrTrust cannot make it trusted. -`files/build-gitea.yml` can publish generic packages when `REGISTRY_TOKEN` is available. +## Important Limits -The workflow: +- This only helps for programs signed with the matching MrSphay certificate chain. +- It does not make unsigned programs trusted. +- It does not disable Defender, SmartScreen, UAC, or enterprise policies. +- Public distribution without warnings is still best handled with a recognized commercial code-signing certificate. -- builds project artifacts, -- copies them to URL-safe filenames, -- uploads immutable versioned packages, -- updates a stable `latest` package path. +## Recommended Project Integration -The workflow uses: - -```text -GITHUB_SERVER_URL -GITHUB_REPOSITORY_OWNER -GITHUB_REPOSITORY -REGISTRY_TOKEN -``` - -When those values are unavailable, replace `REPOSITORY_OWNER`, `REPOSITORY_NAME`, and related placeholders before use. The default Gitea server is `https://git.wilkensxl.de`. - -

-----------------------------------------------------

- -## Agent Follow-up Issues - -Agents should create focused tracker issues for real follow-up work that is outside the current scope or can be handled independently by humans or other agents. - -An issue should include: - -- observed problem, -- impact, -- affected files or commands, -- suggested next steps, -- verification already performed. - -Agents must not create issues for vague reminders, duplicate work, or tasks they can safely finish immediately. Sensitive details belong in private channels or `docs/agent-handoff.md`, not public issues. - -

-----------------------------------------------------

- -## Release Checklist For A New Repo - -Before the first release of a target project: - -1. Ensure `AGENTS.md` and `.codex/project.md` match the real project. -2. Replace all placeholders or mark genuinely unknown values as `PENDING`. -3. Configure `REGISTRY_TOKEN` if packages are published. -4. Configure `GITEA_TOKEN` only if workflows need issue or release API access. -5. Verify SSH push access. -6. Run lint, test, build, and audit commands that exist. -7. Run `git diff --check`. -8. Confirm release artifacts do not include Codex kit metadata unless explicitly wanted. -9. Push and poll workflows to success or document the blocker. - -

-----------------------------------------------------

- -## Updating The Kit In A Project - -When this kit changes, update target repositories conservatively: - -```bash -git status --short -git pull --ff-only -``` - -Then ask Codex: - -```text -Update this repository's Codex Agent Repository Kit files from the latest kit. -Preserve project-specific README content, commands, release rules, and workflow customizations. -Do not overwrite unrelated changes. -``` +Use `docs/integration-prompt.md` in another Windows project. The prompt tells Codex or another assistant to add a visible trust check, a link or bundled copy of MrTrust, and a signing step without hiding security changes from the user. diff --git a/assets/certificates/MrSphay-CodeSigning.cer b/assets/certificates/MrSphay-CodeSigning.cer new file mode 100644 index 0000000000000000000000000000000000000000..a408d841ede4f7960b805c04119a738738382ee7 GIT binary patch literal 1335 zcmV-71<3j^f(0`$f&&|%0|Eg80uUJ9%3v*JB)CnSsubq2-0Uz71_>&LNQUZyvQ*dZuc_2)0V_|F{RC0B5bRbf1Z*(vo7Y#BtFf}nXGBGhY zGB{cn4Kpz?H8C|ZF)}zZI9f0tF&!`)1_MRa#L_zX>MmQ zf&wBi4F(A+hDe6@4FLfG1potr0uKN%f&vNxf&u{m%0DkCaP?<}E7EafnuGd9wB~S( zGF%zi4iNO+GQJy*=zodQwgOVmt_2mc3z}sf5=cgnQmEhY~`DT&shtre+lJuwRu{vEC&`7{AKe4 z6R17t3Z@`7Z~v2p!sOssWrL;mnR7Dg$q zLFWVKtU$Cuz3VEiOPhBpxQt zJwF7JaNEz*b_eaMR|gPHA#SBTeDjO=aDv!;;O3C7nF8nM!}&v(j|+Ob|8Kv$eij#_#v`!-Jyn zNGLJqJl`GnKt%bNffpbd2H&!bSVYO40s{d60i$OyWiSo~163Ul0RjI61OoyGfG`sV z163U*1Pm|=1_&z#0R;sI0|PK01_Mqc9S#H*1Qag;sI{e9f~Y7hn+YZ`)EuvE-Ul!Z1_>&LNQUwihzymn5|5RV&Wb{2_p7!mqe}>CK8{tJyPARox$^oE; zXP|Hz0vhD;Hw^=0a&r*IE8eWHn_(`)7kOB>Xk4v3XN7^9U#BQ!f<_SoPBv(tfu2gK z2%zZ5CGb7tpI?f_Wwnk=`GEEC^Nlzij$iS%mhMP4?+52VS0jwA@=SSYvJ-~5k|<-~+ta-c#YNFLA@WDtiW6&TlyXN*ip)Z=U&S(u z3|hC@<3Ft==yQAq^>TQy*&;<76;p4cPf(}Skrsn^8Ih*ocz^uk?jFv4m|NP{z>z-( zh6dlr^GCF%O)1+U2K_7!BX+y2u*l?*04AjYzV*v;+ELzb<(W{_-Yz{mU|DSy$fx5a zBR%lGUbNVMe0!$?9WNw7oadf++({$|S*6U1K`W7g)yva0Ej0sXu%n+QB{r>+cu1kB ziLQr5wBZv=_}dbuvtbbgzKw>!yf z(_^Eu-MsC*c1P=XsJYsjC%sPucr^lKH?@%`6=}9soeP2-jljDaL*@zbG0krx0jAVA tyL5Bfx7wrydehh$xj>K4?V?zgh6n>O%rKiH27Q}t8W&t52gZb1T#IV`M>qfg literal 0 HcmV?d00001 diff --git a/assets/certificates/MrSphay-LocalTrust-Root.cer b/assets/certificates/MrSphay-LocalTrust-Root.cer new file mode 100644 index 0000000000000000000000000000000000000000..fc3213678c9482ae64c7a0af06b56cc6e519454f GIT binary patch literal 1305 zcmXqLVih%LV*0y)nTe5!Nuc~ezQEN#kzI~QQa&mAZ=Gkr%f_kI=F#?@mywa1mBB#S zP|-l1jX9KsnMcC6D7YXau~NY&KRGc+A*86ZxI`f+KflC4PMp`s%)r#p)X31#!pI^@ zoY&Y8!ZormGK(V4kR~Q2WXl;@8JL@x_!)q1Y{uZBq>$9EK$A+c1mfyHOecwbLrPl9qt6Cp6>;2w;cj~6udK~j5U-!o>XzI~h z@0R?*i9wHR!J59Z@*u;t3T7+vr_BGD%+PU(*_k;nsr3w}S%<*HpM|oU3{J0`J;kcy zW&Y>SEaf{BS9rDm(9Wq}P#oUY&icAz(GstVGP=uhqCTE|e}ePq38fTGxy93q*W6^| zs!1%Ll3Bd@64R{r8@%7$Z*!g7`H?Z;anzeO#_mV!V~euRySH7F-fM99$6wy`=brwiD#azwexvr*@qST8~r|-ncYsR|GQDm zA%1eMY6~{^a-c=JgxR*BpGR>Gqmg@a1WXrp&~Tv)&sA`|mm~ylVRI#KJ4j^JmVl z?(Mn~yrMNhU`@xgtJ!hPpKPR8Fn0wB%{kh>sjTiL+eY=RXW0|J8OqIPweYfudcXF_ z3hvyP8++St2j2Y}==Y(=viGyLotM4pl>9frnamN4TC?^ruW{=*Q~Yadm15`&27c!S z*0xN{j0}v6T@9QK_<-qAmY`>}@v(@ph=eX$ zutiFZ!S$emTjPt_VkI{g-xEVlXuwnsOlXV@b9WqQtg@bWH)P6fuYl{jvOk$J&5hb8 zd14v=c_l}q2-nXO^cv4+N?u-5VqvrSS}u!t-GQX0$(`#|vR5SRm=tP#U88dL&*sgI zLPsrHMQ__jUJ6_}W7*E69xew_1H`V`pzguhh9anayKlY1L$?!PaXmqA)&zhJu zftQcpDF}GO&FWvhZo{sr(I0-Uybx(~?5KhKTlF=(*G_7kRctWgoosHpclX3=g`(>H zatd5sYGK=Mu3E9#>1!AJx`wTmD_FTtv9I%aqG56@yi@wc9FF$}+vkRC&@L32n7eF7 z@8kgGhC{0lPFwJwbG}5O62oNw^3Uo2XWstA;D1oxcH0!j;-t8B4YCKaC%*Ud5S}>E zAnQs%K98B(wcL&ii@QxN+rzeA4yru+ur1=u3nLSwtG~K5LOcS+9HpY|GA}h;;Sy^U z3YuxNZXMg{1F{+BYwv7{3YuE1Biujrx2}v)S)Rf7l&y)MgMUsnh?B`%+`lk)HG6a7 z=XsxIi*E6jnQ~v|BzybC*%z;FU3Btt^DaK^H>sy0|2=Hqs8t>^kKyKu>0%=OXYM=k zuvC03z4j|+Bd62!JjsPu -CertificateThumbprint A024A89200469F099EC4A172B4F96F6428AFD41B +- Treat the certificate thumbprint as public metadata, but never commit private signing material. + +Verification: +- Confirm unsigned builds still show as unsigned. +- Confirm signed builds validate after MrTrust installation. +- Confirm the MrTrust certificate can be removed again. +- Confirm no private signing material is present in the repository or release artifact. +``` diff --git a/docs/security-model.md b/docs/security-model.md new file mode 100644 index 0000000..5b1f2be --- /dev/null +++ b/docs/security-model.md @@ -0,0 +1,40 @@ +# MrTrust Security Model + +MrTrust is a trust bootstrapper, not a security bypass. + +## Allowed Behavior + +- Import a public MrSphay certificate into Windows certificate stores after explicit user approval. +- Sign MrSphay build artifacts with a private code-signing certificate kept outside git. +- Provide an uninstall script that removes the same certificate again. + +## Disallowed Behavior + +- Disabling Microsoft Defender. +- Disabling SmartScreen. +- Silently modifying certificate stores. +- Installing private keys on user machines. +- Hiding certificate installation inside unrelated app actions. +- Shipping `.pfx` files or signing passwords in a repository or release. + +## Recommended Stores + +For normal users: + +```text +Cert:\CurrentUser\Root +Cert:\CurrentUser\TrustedPublisher +``` + +For managed PCs or all-user installs: + +```text +Cert:\LocalMachine\Root +Cert:\LocalMachine\TrustedPublisher +``` + +The LocalMachine stores require administrator approval. + +## Residual Windows Warnings + +Even after MrTrust is installed, Windows can still block suspicious software. SmartScreen reputation, Defender detections, enterprise security policy, and downloaded-file mark-of-the-web behavior are separate from Authenticode trust. diff --git a/scripts/Build-MrTrustExe.ps1 b/scripts/Build-MrTrustExe.ps1 new file mode 100644 index 0000000..2d4190a --- /dev/null +++ b/scripts/Build-MrTrustExe.ps1 @@ -0,0 +1,50 @@ +[CmdletBinding()] +param( + [string]$OutputPath = ".\dist\MrTrust.exe" +) + +$ErrorActionPreference = "Stop" + +function Resolve-FullPath { + param([Parameter(Mandatory)][string]$Path) + + $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) +} + +$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$sourcePath = Join-Path $root "src\MrTrustLauncher.cs" +$resolvedOutputPath = Resolve-FullPath $OutputPath +$outputDirectory = Split-Path -Parent $resolvedOutputPath + +if (-not (Test-Path -LiteralPath $sourcePath)) { + throw "Launcher source not found: $sourcePath" +} + +New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null + +$compilerCandidates = @( + "$env:WINDIR\Microsoft.NET\Framework64\v4.0.30319\csc.exe", + "$env:WINDIR\Microsoft.NET\Framework\v4.0.30319\csc.exe" +) + +$compiler = $compilerCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 +if (-not $compiler) { + throw "csc.exe was not found. Run this build on a Windows Gitea runner with .NET Framework installed." +} + +& $compiler ` + /nologo ` + /target:winexe ` + /optimize+ ` + /platform:anycpu ` + /out:$resolvedOutputPath ` + /reference:System.Windows.Forms.dll ` + /reference:System.Drawing.dll ` + $sourcePath + +if ($LASTEXITCODE -ne 0) { + throw "csc.exe failed with exit code $LASTEXITCODE." +} + +Write-Host "Created EXE:" +Write-Host " $resolvedOutputPath" diff --git a/scripts/Install-MrTrust.ps1 b/scripts/Install-MrTrust.ps1 new file mode 100644 index 0000000..56b681b --- /dev/null +++ b/scripts/Install-MrTrust.ps1 @@ -0,0 +1,90 @@ +[CmdletBinding(SupportsShouldProcess)] +param( + [string]$CertificatePath = ".\assets\certificates\MrSphay-LocalTrust-Root.cer", + [string]$PublisherCertificatePath = ".\assets\certificates\MrSphay-CodeSigning.cer", + [ValidateSet("CurrentUser", "LocalMachine")] + [string]$Scope = "CurrentUser" +) + +$ErrorActionPreference = "Stop" + +function Resolve-FullPath { + param([Parameter(Mandatory)][string]$Path) + + $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) +} + +function Test-IsAdministrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +$resolvedCertificatePath = Resolve-FullPath $CertificatePath +if (-not (Test-Path -LiteralPath $resolvedCertificatePath)) { + throw "Certificate file not found: $resolvedCertificatePath. Run scripts\New-MrTrustCertificate.ps1 first or provide -CertificatePath." +} + +if ($Scope -eq "LocalMachine" -and -not (Test-IsAdministrator)) { + throw "LocalMachine installation requires an elevated PowerShell session. Use -Scope CurrentUser or run as Administrator." +} + +$rootCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedCertificatePath) +if (-not $rootCertificate.Subject.StartsWith("CN=MrSphay", [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to install an unexpected root certificate subject: $($rootCertificate.Subject)" +} + +$resolvedPublisherCertificatePath = Resolve-FullPath $PublisherCertificatePath +$publisherCertificate = $null +if (Test-Path -LiteralPath $resolvedPublisherCertificatePath) { + $publisherCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedPublisherCertificatePath) + if (-not $publisherCertificate.Subject.StartsWith("CN=MrSphay", [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to install an unexpected publisher certificate subject: $($publisherCertificate.Subject)" + } +} + +$rootStore = "Cert:\$Scope\Root" +$publisherStore = "Cert:\$Scope\TrustedPublisher" + +Write-Host "MrTrust will install this certificate as trusted for scope '$Scope':" +Write-Host " Root subject: $($rootCertificate.Subject)" +Write-Host " Root issuer: $($rootCertificate.Issuer)" +Write-Host " Root thumbprint: $($rootCertificate.Thumbprint)" +Write-Host " Root expires: $($rootCertificate.NotAfter.ToString('yyyy-MM-dd HH:mm:ss'))" +if ($publisherCertificate) { + Write-Host " Publisher subject: $($publisherCertificate.Subject)" + Write-Host " Publisher thumbprint: $($publisherCertificate.Thumbprint)" +} +else { + Write-Warning "Publisher certificate not found at $resolvedPublisherCertificatePath. Only the root certificate will be installed." +} +Write-Host "" +Write-Warning "Only continue if you trust MrSphay software signed with this certificate chain." + +$answer = Read-Host "Type INSTALL to continue" +if ($answer -cne "INSTALL") { + Write-Host "Installation cancelled." + exit 1 +} + +$existingRoot = Get-ChildItem -Path $rootStore | Where-Object Thumbprint -eq $rootCertificate.Thumbprint + +if (-not $existingRoot) { + if ($PSCmdlet.ShouldProcess($rootStore, "Install MrTrust root certificate")) { + Import-Certificate -FilePath $resolvedCertificatePath -CertStoreLocation $rootStore | Out-Null + } +} + +if ($publisherCertificate) { + $existingPublisher = Get-ChildItem -Path $publisherStore | Where-Object Thumbprint -eq $publisherCertificate.Thumbprint + if (-not $existingPublisher) { + if ($PSCmdlet.ShouldProcess($publisherStore, "Install MrTrust trusted publisher certificate")) { + Import-Certificate -FilePath $resolvedPublisherCertificatePath -CertStoreLocation $publisherStore | Out-Null + } + } +} + +Write-Host "MrTrust certificate installed." +Write-Host "Installed stores:" +Write-Host " $rootStore" +Write-Host " $publisherStore" diff --git a/scripts/New-MrTrustCertificate.ps1 b/scripts/New-MrTrustCertificate.ps1 new file mode 100644 index 0000000..e154890 --- /dev/null +++ b/scripts/New-MrTrustCertificate.ps1 @@ -0,0 +1,90 @@ +[CmdletBinding()] +param( + [string]$PublisherName = "MrSphay", + [string]$RootSubject = "CN=MrSphay Local Trust Root", + [string]$SigningSubject = "CN=MrSphay Code Signing", + [string]$PublicCertificateDirectory = ".\assets\certificates", + [string]$PrivateDirectory = ".\private", + [int]$ValidYears = 5, + [switch]$SkipPfxExport +) + +$ErrorActionPreference = "Stop" + +function Resolve-FullPath { + param([Parameter(Mandatory)][string]$Path) + + $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) +} + +if ($ValidYears -lt 1 -or $ValidYears -gt 20) { + throw "ValidYears must be between 1 and 20." +} + +$publicDirectory = Resolve-FullPath $PublicCertificateDirectory +$privateDirectory = Resolve-FullPath $PrivateDirectory + +New-Item -ItemType Directory -Force -Path $publicDirectory | Out-Null +if (-not $SkipPfxExport) { + New-Item -ItemType Directory -Force -Path $privateDirectory | Out-Null +} + +$rootCertPath = Join-Path $publicDirectory "$PublisherName-LocalTrust-Root.cer" +$signingCertPath = Join-Path $publicDirectory "$PublisherName-CodeSigning.cer" +$pfxPath = Join-Path $privateDirectory "$PublisherName-CodeSigning.pfx" + +Write-Host "Creating local trust root certificate: $RootSubject" +$rootCertificate = New-SelfSignedCertificate ` + -Type Custom ` + -Subject $RootSubject ` + -KeyAlgorithm RSA ` + -KeyLength 4096 ` + -HashAlgorithm SHA256 ` + -KeyExportPolicy Exportable ` + -KeyUsage CertSign, CRLSign, DigitalSignature ` + -TextExtension @("2.5.29.19={critical}{text}ca=1&pathlength=1") ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -NotAfter (Get-Date).AddYears($ValidYears) + +Write-Host "Creating code-signing certificate: $SigningSubject" +$signingCertificate = New-SelfSignedCertificate ` + -Type CodeSigningCert ` + -Subject $SigningSubject ` + -Signer $rootCertificate ` + -KeyAlgorithm RSA ` + -KeyLength 4096 ` + -HashAlgorithm SHA256 ` + -KeyExportPolicy Exportable ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -NotAfter (Get-Date).AddYears($ValidYears) + +Export-Certificate -Cert $rootCertificate -FilePath $rootCertPath -Force | Out-Null +Export-Certificate -Cert $signingCertificate -FilePath $signingCertPath -Force | Out-Null + +if (-not $SkipPfxExport) { + $password = Read-Host "Enter a strong password for the private code-signing PFX" -AsSecureString + Export-PfxCertificate -Cert $signingCertificate -FilePath $pfxPath -Password $password -Force | Out-Null +} + +Write-Host "" +Write-Host "Created public root certificate:" +Write-Host " $rootCertPath" +Write-Host "Root thumbprint:" +Write-Host " $($rootCertificate.Thumbprint)" +Write-Host "" +Write-Host "Created public signing certificate:" +Write-Host " $signingCertPath" +Write-Host "Signing thumbprint:" +Write-Host " $($signingCertificate.Thumbprint)" +Write-Host "" +if ($SkipPfxExport) { + Write-Host "Skipped private PFX export." + Write-Host "Use this thumbprint for signing from the Windows certificate store:" + Write-Host " $($signingCertificate.Thumbprint)" +} +else { + Write-Host "Created private PFX:" + Write-Host " $pfxPath" + Write-Host "" + Write-Warning "Do not commit, publish, or share the private .pfx file or its password." +} diff --git a/scripts/New-MrTrustRelease.ps1 b/scripts/New-MrTrustRelease.ps1 new file mode 100644 index 0000000..6028905 --- /dev/null +++ b/scripts/New-MrTrustRelease.ps1 @@ -0,0 +1,70 @@ +[CmdletBinding()] +param( + [string]$Version = "0.1.0", + [string]$OutputDirectory = ".\dist", + [string]$SigningThumbprint, + [switch]$NoTimestamp, + [switch]$AllowUntrustedRoot +) + +$ErrorActionPreference = "Stop" + +function Resolve-FullPath { + param([Parameter(Mandatory)][string]$Path) + + $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) +} + +$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$output = Resolve-FullPath $OutputDirectory +$packageRoot = Join-Path $output "MrTrust-$Version" +$zipPath = Join-Path $output "MrTrust-$Version.zip" +$exePath = Join-Path $output "MrTrust.exe" + +if (Test-Path -LiteralPath $packageRoot) { + Remove-Item -LiteralPath $packageRoot -Recurse -Force +} + +New-Item -ItemType Directory -Force -Path $packageRoot | Out-Null +New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot "scripts") | Out-Null +New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot "assets\certificates") | Out-Null +New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot "docs") | Out-Null + +& (Join-Path $root "scripts\Build-MrTrustExe.ps1") -OutputPath $exePath + +if ($SigningThumbprint) { + $signArguments = @{ + Path = $exePath + CertificateThumbprint = $SigningThumbprint + } + + if ($NoTimestamp) { + $signArguments.NoTimestamp = $true + } + + if ($AllowUntrustedRoot) { + $signArguments.AllowUntrustedRoot = $true + } + + & (Join-Path $root "scripts\Sign-MrTrustProject.ps1") @signArguments +} + +Copy-Item -LiteralPath $exePath -Destination $packageRoot +Copy-Item -LiteralPath (Join-Path $root "MrTrust.ps1") -Destination $packageRoot +Copy-Item -LiteralPath (Join-Path $root "README.md") -Destination $packageRoot +Copy-Item -LiteralPath (Join-Path $root "scripts\Install-MrTrust.ps1") -Destination (Join-Path $packageRoot "scripts") +Copy-Item -LiteralPath (Join-Path $root "scripts\Uninstall-MrTrust.ps1") -Destination (Join-Path $packageRoot "scripts") +Copy-Item -LiteralPath (Join-Path $root "scripts\Start-MrTrustGui.ps1") -Destination (Join-Path $packageRoot "scripts") +Copy-Item -LiteralPath (Join-Path $root "assets\certificates\MrSphay-LocalTrust-Root.cer") -Destination (Join-Path $packageRoot "assets\certificates") +Copy-Item -LiteralPath (Join-Path $root "assets\certificates\MrSphay-CodeSigning.cer") -Destination (Join-Path $packageRoot "assets\certificates") +Copy-Item -LiteralPath (Join-Path $root "assets\certificates\thumbprints.txt") -Destination (Join-Path $packageRoot "assets\certificates") +Copy-Item -LiteralPath (Join-Path $root "docs\security-model.md") -Destination (Join-Path $packageRoot "docs") + +if (Test-Path -LiteralPath $zipPath) { + Remove-Item -LiteralPath $zipPath -Force +} + +Compress-Archive -Path (Join-Path $packageRoot "*") -DestinationPath $zipPath -Force + +Write-Host "Created release package:" +Write-Host " $zipPath" diff --git a/scripts/Sign-MrTrustProject.ps1 b/scripts/Sign-MrTrustProject.ps1 new file mode 100644 index 0000000..e62d7f4 --- /dev/null +++ b/scripts/Sign-MrTrustProject.ps1 @@ -0,0 +1,104 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string[]]$Path, + [string]$PfxPath, + [string]$CertificateThumbprint, + [string]$TimestampServer = "http://timestamp.digicert.com", + [switch]$NoTimestamp, + [switch]$AllowUntrustedRoot +) + +$ErrorActionPreference = "Stop" + +function Resolve-FullPath { + param([Parameter(Mandatory)][string]$Path) + + $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) +} + +function Get-CodeSigningCertificateFromStore { + param([Parameter(Mandatory)][string]$Thumbprint) + + $normalizedThumbprint = $Thumbprint -replace "\s", "" + $certificate = Get-ChildItem "Cert:\CurrentUser\My", "Cert:\LocalMachine\My" | + Where-Object { $_.Thumbprint -eq $normalizedThumbprint } | + Select-Object -First 1 + + if (-not $certificate) { + throw "No code-signing certificate found with thumbprint $Thumbprint." + } + + $certificate +} + +if (-not $PfxPath -and -not $CertificateThumbprint) { + throw "Provide either -PfxPath or -CertificateThumbprint." +} + +if ($PfxPath -and $CertificateThumbprint) { + throw "Use either -PfxPath or -CertificateThumbprint, not both." +} + +if ($PfxPath) { + $resolvedPfxPath = Resolve-FullPath $PfxPath + if (-not (Test-Path -LiteralPath $resolvedPfxPath)) { + throw "PFX file not found: $resolvedPfxPath" + } + + $password = Read-Host "Enter PFX password" -AsSecureString + $storageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bor + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet + $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedPfxPath, $password, $storageFlags) +} +else { + $certificate = Get-CodeSigningCertificateFromStore -Thumbprint $CertificateThumbprint +} + +if (-not $certificate.HasPrivateKey) { + throw "The selected certificate does not include a private key and cannot sign files." +} + +$targets = foreach ($item in $Path) { + $resolvedPath = Resolve-FullPath $item + if (Test-Path -LiteralPath $resolvedPath -PathType Container) { + Get-ChildItem -LiteralPath $resolvedPath -Recurse -File | + Where-Object { $_.Extension -in ".exe", ".msi", ".dll", ".ps1", ".psm1", ".psd1", ".cat" } + } + elseif (Test-Path -LiteralPath $resolvedPath -PathType Leaf) { + Get-Item -LiteralPath $resolvedPath + } + else { + throw "Path not found: $resolvedPath" + } +} + +if (-not $targets) { + throw "No files found to sign." +} + +foreach ($target in $targets) { + Write-Host "Signing $($target.FullName)" + $signatureArguments = @{ + FilePath = $target.FullName + Certificate = $certificate + HashAlgorithm = "SHA256" + } + + if (-not $NoTimestamp -and $TimestampServer) { + $signatureArguments.TimestampServer = $TimestampServer + } + + $signature = Set-AuthenticodeSignature @signatureArguments + + if ($signature.Status -eq "UnknownError" -and $AllowUntrustedRoot -and $signature.SignerCertificate) { + Write-Warning "Signed $($target.FullName), but the local machine does not trust the signing root yet." + continue + } + + if ($signature.Status -ne "Valid") { + throw "Signing failed for $($target.FullName): $($signature.StatusMessage)" + } +} + +Write-Host "Signed $($targets.Count) file(s)." diff --git a/scripts/Start-MrTrustGui.ps1 b/scripts/Start-MrTrustGui.ps1 new file mode 100644 index 0000000..520455c --- /dev/null +++ b/scripts/Start-MrTrustGui.ps1 @@ -0,0 +1,324 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" + +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +$script:RootPath = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$script:RootCertificatePath = Join-Path $script:RootPath "assets\certificates\MrSphay-LocalTrust-Root.cer" +$script:PublisherCertificatePath = Join-Path $script:RootPath "assets\certificates\MrSphay-CodeSigning.cer" + +function Test-IsAdministrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Get-MrTrustCertificate { + param([Parameter(Mandatory)][string]$Path) + + if (-not (Test-Path -LiteralPath $Path)) { + throw "Certificate file not found: $Path" + } + + [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($Path) +} + +function Get-TrustScope { + if ($script:AllUsersCheckBox.Checked) { + "LocalMachine" + } + else { + "CurrentUser" + } +} + +function Get-StorePath { + param( + [Parameter(Mandatory)][string]$Scope, + [Parameter(Mandatory)][string]$Store + ) + + "Cert:\$Scope\$Store" +} + +function Test-CertificateInstalled { + param( + [Parameter(Mandatory)]$Certificate, + [Parameter(Mandatory)][string]$Scope, + [Parameter(Mandatory)][string]$Store + ) + + $storePath = Get-StorePath -Scope $Scope -Store $Store + @(Get-ChildItem -Path $storePath | Where-Object Thumbprint -eq $Certificate.Thumbprint).Count -gt 0 +} + +function Set-StatusText { + param([Parameter(Mandatory)][string]$Text) + + $script:StatusLabel.Text = $Text +} + +function Refresh-MrTrustStatus { + try { + $rootCertificate = Get-MrTrustCertificate -Path $script:RootCertificatePath + $publisherCertificate = Get-MrTrustCertificate -Path $script:PublisherCertificatePath + $scope = Get-TrustScope + + $rootInstalled = Test-CertificateInstalled -Certificate $rootCertificate -Scope $scope -Store "Root" + $publisherInstalled = Test-CertificateInstalled -Certificate $publisherCertificate -Scope $scope -Store "TrustedPublisher" + + $script:RootThumbprintLabel.Text = $rootCertificate.Thumbprint + $script:PublisherThumbprintLabel.Text = $publisherCertificate.Thumbprint + $script:ExpiryLabel.Text = $rootCertificate.NotAfter.ToString("yyyy-MM-dd") + + if ($rootInstalled -and $publisherInstalled) { + Set-StatusText "Trusted for $scope" + $script:StatusPill.BackColor = [Drawing.Color]::FromArgb(28, 185, 111) + } + else { + Set-StatusText "Not installed for $scope" + $script:StatusPill.BackColor = [Drawing.Color]::FromArgb(242, 153, 74) + } + } + catch { + Set-StatusText $_.Exception.Message + $script:StatusPill.BackColor = [Drawing.Color]::FromArgb(235, 87, 87) + } +} + +function Install-MrTrustCertificates { + $scope = Get-TrustScope + if ($scope -eq "LocalMachine" -and -not (Test-IsAdministrator)) { + [Windows.Forms.MessageBox]::Show( + "All-users trust requires running PowerShell as Administrator.", + "MrTrust", + [Windows.Forms.MessageBoxButtons]::OK, + [Windows.Forms.MessageBoxIcon]::Warning + ) | Out-Null + return + } + + $rootCertificate = Get-MrTrustCertificate -Path $script:RootCertificatePath + $publisherCertificate = Get-MrTrustCertificate -Path $script:PublisherCertificatePath + + $message = "Install MrSphay trust for $scope?`r`n`r`nRoot:`r`n$($rootCertificate.Thumbprint)`r`n`r`nPublisher:`r`n$($publisherCertificate.Thumbprint)`r`n`r`nOnly continue if you trust software signed by MrSphay." + $result = [Windows.Forms.MessageBox]::Show( + $message, + "Install MrTrust", + [Windows.Forms.MessageBoxButtons]::YesNo, + [Windows.Forms.MessageBoxIcon]::Warning + ) + + if ($result -ne [Windows.Forms.DialogResult]::Yes) { + return + } + + Import-Certificate -FilePath $script:RootCertificatePath -CertStoreLocation (Get-StorePath -Scope $scope -Store "Root") | Out-Null + Import-Certificate -FilePath $script:PublisherCertificatePath -CertStoreLocation (Get-StorePath -Scope $scope -Store "TrustedPublisher") | Out-Null + Refresh-MrTrustStatus +} + +function Remove-MrTrustCertificates { + $scope = Get-TrustScope + if ($scope -eq "LocalMachine" -and -not (Test-IsAdministrator)) { + [Windows.Forms.MessageBox]::Show( + "All-users removal requires running PowerShell as Administrator.", + "MrTrust", + [Windows.Forms.MessageBoxButtons]::OK, + [Windows.Forms.MessageBoxIcon]::Warning + ) | Out-Null + return + } + + $rootCertificate = Get-MrTrustCertificate -Path $script:RootCertificatePath + $publisherCertificate = Get-MrTrustCertificate -Path $script:PublisherCertificatePath + $result = [Windows.Forms.MessageBox]::Show( + "Remove MrSphay trust for $scope?", + "Remove MrTrust", + [Windows.Forms.MessageBoxButtons]::YesNo, + [Windows.Forms.MessageBoxIcon]::Question + ) + + if ($result -ne [Windows.Forms.DialogResult]::Yes) { + return + } + + $targets = @( + [pscustomobject]@{ Store = "Root"; Thumbprint = $rootCertificate.Thumbprint }, + [pscustomobject]@{ Store = "TrustedPublisher"; Thumbprint = $publisherCertificate.Thumbprint } + ) + + foreach ($target in $targets) { + $storePath = Get-StorePath -Scope $scope -Store $target.Store + Get-ChildItem -Path $storePath | + Where-Object Thumbprint -eq $target.Thumbprint | + Remove-Item + } + + Refresh-MrTrustStatus +} + +[Windows.Forms.Application]::EnableVisualStyles() + +$form = [Windows.Forms.Form]::new() +$form.Text = "MrTrust" +$form.StartPosition = "CenterScreen" +$form.ClientSize = [Drawing.Size]::new(760, 520) +$form.MinimumSize = [Drawing.Size]::new(720, 500) +$form.BackColor = [Drawing.Color]::FromArgb(22, 26, 29) +$form.Font = [Drawing.Font]::new("Segoe UI", 10) + +$header = [Windows.Forms.Panel]::new() +$header.Dock = "Top" +$header.Height = 108 +$header.BackColor = [Drawing.Color]::FromArgb(27, 32, 35) +$form.Controls.Add($header) + +$accent = [Windows.Forms.Panel]::new() +$accent.Dock = "Left" +$accent.Width = 8 +$accent.BackColor = [Drawing.Color]::FromArgb(28, 185, 111) +$header.Controls.Add($accent) + +$title = [Windows.Forms.Label]::new() +$title.Text = "MrTrust" +$title.ForeColor = [Drawing.Color]::White +$title.Font = [Drawing.Font]::new("Segoe UI", 24, [Drawing.FontStyle]::Bold) +$title.AutoSize = $true +$title.Location = [Drawing.Point]::new(30, 18) +$header.Controls.Add($title) + +$subtitle = [Windows.Forms.Label]::new() +$subtitle.Text = "Trust setup for MrSphay signed Windows apps" +$subtitle.ForeColor = [Drawing.Color]::FromArgb(177, 190, 183) +$subtitle.AutoSize = $true +$subtitle.Location = [Drawing.Point]::new(34, 66) +$header.Controls.Add($subtitle) + +$script:StatusPill = [Windows.Forms.Panel]::new() +$script:StatusPill.Size = [Drawing.Size]::new(14, 14) +$script:StatusPill.Location = [Drawing.Point]::new(610, 42) +$script:StatusPill.BackColor = [Drawing.Color]::FromArgb(242, 153, 74) +$header.Controls.Add($script:StatusPill) + +$script:StatusLabel = [Windows.Forms.Label]::new() +$script:StatusLabel.Text = "Checking..." +$script:StatusLabel.ForeColor = [Drawing.Color]::FromArgb(225, 231, 227) +$script:StatusLabel.AutoSize = $true +$script:StatusLabel.Location = [Drawing.Point]::new(632, 38) +$header.Controls.Add($script:StatusLabel) + +$content = [Windows.Forms.Panel]::new() +$content.Dock = "Fill" +$content.Padding = [Windows.Forms.Padding]::new(30) +$content.BackColor = [Drawing.Color]::FromArgb(22, 26, 29) +$form.Controls.Add($content) + +$infoPanel = [Windows.Forms.Panel]::new() +$infoPanel.BackColor = [Drawing.Color]::FromArgb(31, 37, 40) +$infoPanel.Size = [Drawing.Size]::new(700, 210) +$infoPanel.Location = [Drawing.Point]::new(30, 34) +$content.Controls.Add($infoPanel) + +$scopeLabel = [Windows.Forms.Label]::new() +$scopeLabel.Text = "Scope" +$scopeLabel.ForeColor = [Drawing.Color]::FromArgb(177, 190, 183) +$scopeLabel.Location = [Drawing.Point]::new(24, 24) +$scopeLabel.AutoSize = $true +$infoPanel.Controls.Add($scopeLabel) + +$script:AllUsersCheckBox = [Windows.Forms.CheckBox]::new() +$script:AllUsersCheckBox.Text = "Install for all users (requires Administrator)" +$script:AllUsersCheckBox.ForeColor = [Drawing.Color]::FromArgb(225, 231, 227) +$script:AllUsersCheckBox.Location = [Drawing.Point]::new(24, 50) +$script:AllUsersCheckBox.AutoSize = $true +$script:AllUsersCheckBox.FlatStyle = "Flat" +$script:AllUsersCheckBox.Add_CheckedChanged({ Refresh-MrTrustStatus }) +$infoPanel.Controls.Add($script:AllUsersCheckBox) + +$rootLabel = [Windows.Forms.Label]::new() +$rootLabel.Text = "Root thumbprint" +$rootLabel.ForeColor = [Drawing.Color]::FromArgb(177, 190, 183) +$rootLabel.Location = [Drawing.Point]::new(24, 92) +$rootLabel.AutoSize = $true +$infoPanel.Controls.Add($rootLabel) + +$script:RootThumbprintLabel = [Windows.Forms.Label]::new() +$script:RootThumbprintLabel.Text = "-" +$script:RootThumbprintLabel.ForeColor = [Drawing.Color]::FromArgb(225, 231, 227) +$script:RootThumbprintLabel.Font = [Drawing.Font]::new("Consolas", 9) +$script:RootThumbprintLabel.Location = [Drawing.Point]::new(180, 92) +$script:RootThumbprintLabel.AutoSize = $true +$infoPanel.Controls.Add($script:RootThumbprintLabel) + +$publisherLabel = [Windows.Forms.Label]::new() +$publisherLabel.Text = "Publisher thumbprint" +$publisherLabel.ForeColor = [Drawing.Color]::FromArgb(177, 190, 183) +$publisherLabel.Location = [Drawing.Point]::new(24, 128) +$publisherLabel.AutoSize = $true +$infoPanel.Controls.Add($publisherLabel) + +$script:PublisherThumbprintLabel = [Windows.Forms.Label]::new() +$script:PublisherThumbprintLabel.Text = "-" +$script:PublisherThumbprintLabel.ForeColor = [Drawing.Color]::FromArgb(225, 231, 227) +$script:PublisherThumbprintLabel.Font = [Drawing.Font]::new("Consolas", 9) +$script:PublisherThumbprintLabel.Location = [Drawing.Point]::new(180, 128) +$script:PublisherThumbprintLabel.AutoSize = $true +$infoPanel.Controls.Add($script:PublisherThumbprintLabel) + +$expiryLabelTitle = [Windows.Forms.Label]::new() +$expiryLabelTitle.Text = "Expires" +$expiryLabelTitle.ForeColor = [Drawing.Color]::FromArgb(177, 190, 183) +$expiryLabelTitle.Location = [Drawing.Point]::new(24, 164) +$expiryLabelTitle.AutoSize = $true +$infoPanel.Controls.Add($expiryLabelTitle) + +$script:ExpiryLabel = [Windows.Forms.Label]::new() +$script:ExpiryLabel.Text = "-" +$script:ExpiryLabel.ForeColor = [Drawing.Color]::FromArgb(225, 231, 227) +$script:ExpiryLabel.Location = [Drawing.Point]::new(180, 164) +$script:ExpiryLabel.AutoSize = $true +$infoPanel.Controls.Add($script:ExpiryLabel) + +$installButton = [Windows.Forms.Button]::new() +$installButton.Text = "Install trust" +$installButton.BackColor = [Drawing.Color]::FromArgb(28, 185, 111) +$installButton.ForeColor = [Drawing.Color]::White +$installButton.FlatStyle = "Flat" +$installButton.Size = [Drawing.Size]::new(180, 46) +$installButton.Location = [Drawing.Point]::new(30, 274) +$installButton.Add_Click({ Install-MrTrustCertificates }) +$content.Controls.Add($installButton) + +$removeButton = [Windows.Forms.Button]::new() +$removeButton.Text = "Remove trust" +$removeButton.BackColor = [Drawing.Color]::FromArgb(44, 52, 56) +$removeButton.ForeColor = [Drawing.Color]::FromArgb(225, 231, 227) +$removeButton.FlatStyle = "Flat" +$removeButton.Size = [Drawing.Size]::new(180, 46) +$removeButton.Location = [Drawing.Point]::new(230, 274) +$removeButton.Add_Click({ Remove-MrTrustCertificates }) +$content.Controls.Add($removeButton) + +$refreshButton = [Windows.Forms.Button]::new() +$refreshButton.Text = "Refresh" +$refreshButton.BackColor = [Drawing.Color]::FromArgb(44, 52, 56) +$refreshButton.ForeColor = [Drawing.Color]::FromArgb(225, 231, 227) +$refreshButton.FlatStyle = "Flat" +$refreshButton.Size = [Drawing.Size]::new(140, 46) +$refreshButton.Location = [Drawing.Point]::new(430, 274) +$refreshButton.Add_Click({ Refresh-MrTrustStatus }) +$content.Controls.Add($refreshButton) + +$note = [Windows.Forms.Label]::new() +$note.Text = "MrTrust installs public certificates only. It does not disable Defender, SmartScreen, UAC, or enterprise policies." +$note.ForeColor = [Drawing.Color]::FromArgb(177, 190, 183) +$note.Location = [Drawing.Point]::new(30, 352) +$note.Size = [Drawing.Size]::new(700, 48) +$content.Controls.Add($note) + +$form.Add_Shown({ Refresh-MrTrustStatus }) +[Windows.Forms.Application]::Run($form) diff --git a/scripts/Uninstall-MrTrust.ps1 b/scripts/Uninstall-MrTrust.ps1 new file mode 100644 index 0000000..76334af --- /dev/null +++ b/scripts/Uninstall-MrTrust.ps1 @@ -0,0 +1,87 @@ +[CmdletBinding(SupportsShouldProcess)] +param( + [string]$CertificatePath = ".\assets\certificates\MrSphay-LocalTrust-Root.cer", + [string]$PublisherCertificatePath = ".\assets\certificates\MrSphay-CodeSigning.cer", + [ValidateSet("CurrentUser", "LocalMachine")] + [string]$Scope = "CurrentUser", + [switch]$Force +) + +$ErrorActionPreference = "Stop" + +function Resolve-FullPath { + param([Parameter(Mandatory)][string]$Path) + + $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) +} + +function Test-IsAdministrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +if ($Scope -eq "LocalMachine" -and -not (Test-IsAdministrator)) { + throw "LocalMachine removal requires an elevated PowerShell session. Use -Scope CurrentUser or run as Administrator." +} + +$resolvedCertificatePath = Resolve-FullPath $CertificatePath +if (-not (Test-Path -LiteralPath $resolvedCertificatePath)) { + throw "Certificate file not found: $resolvedCertificatePath. Provide -CertificatePath to the public MrTrust certificate." +} + +$rootCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedCertificatePath) +if (-not $rootCertificate.Subject.StartsWith("CN=MrSphay", [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to remove using an unexpected root certificate subject: $($rootCertificate.Subject)" +} + +$resolvedPublisherCertificatePath = Resolve-FullPath $PublisherCertificatePath +$publisherCertificate = $null +if (Test-Path -LiteralPath $resolvedPublisherCertificatePath) { + $publisherCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedPublisherCertificatePath) + if (-not $publisherCertificate.Subject.StartsWith("CN=MrSphay", [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to remove using an unexpected publisher certificate subject: $($publisherCertificate.Subject)" + } +} + +$targets = @( + [pscustomobject]@{ + Store = "Cert:\$Scope\Root" + Thumbprint = $rootCertificate.Thumbprint + } +) + +if ($publisherCertificate) { + $targets += [pscustomobject]@{ + Store = "Cert:\$Scope\TrustedPublisher" + Thumbprint = $publisherCertificate.Thumbprint + } +} + +Write-Host "MrTrust will remove this certificate from scope '$Scope':" +Write-Host " Root subject: $($rootCertificate.Subject)" +Write-Host " Root thumbprint: $($rootCertificate.Thumbprint)" +if ($publisherCertificate) { + Write-Host " Publisher subject: $($publisherCertificate.Subject)" + Write-Host " Publisher thumbprint: $($publisherCertificate.Thumbprint)" +} +Write-Host "" + +if (-not $Force) { + $answer = Read-Host "Type REMOVE to continue" + if ($answer -cne "REMOVE") { + Write-Host "Removal cancelled." + exit 1 + } +} + +foreach ($target in $targets) { + $matchingCertificates = Get-ChildItem -Path $target.Store | Where-Object Thumbprint -eq $target.Thumbprint + foreach ($matchingCertificate in $matchingCertificates) { + if ($PSCmdlet.ShouldProcess($target.Store, "Remove MrTrust certificate $($matchingCertificate.Thumbprint)")) { + Remove-Item -LiteralPath $matchingCertificate.PSPath + } + } +} + +Write-Host "MrTrust certificate removed where present." diff --git a/src/MrTrustLauncher.cs b/src/MrTrustLauncher.cs new file mode 100644 index 0000000..f8ad4d4 --- /dev/null +++ b/src/MrTrustLauncher.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Windows.Forms; + +namespace MrTrust +{ + internal static class MrTrustLauncher + { + [STAThread] + private static int Main() + { + string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + string scriptPath = Path.Combine(baseDirectory, "MrTrust.ps1"); + + if (!File.Exists(scriptPath)) + { + MessageBox.Show( + "MrTrust.ps1 was not found next to MrTrust.exe.", + "MrTrust", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + return 1; + } + + try + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = "-NoProfile -ExecutionPolicy Bypass -File \"" + scriptPath + "\" gui", + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = baseDirectory + }; + + using (Process process = Process.Start(startInfo)) + { + if (process == null) + { + throw new InvalidOperationException("PowerShell could not be started."); + } + } + + return 0; + } + catch (Exception ex) + { + MessageBox.Show( + ex.Message, + "MrTrust", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + return 1; + } + } + } +}