generated from MrSphay/codex-agent-repository-kit
Add MrTrust GUI and Gitea release build
Some checks failed
Build MrTrust / build-windows (push) Has been cancelled
Some checks failed
Build MrTrust / build-windows (push) Has been cancelled
This commit is contained in:
49
.gitea/workflows/build.yml
Normal file
49
.gitea/workflows/build.yml
Normal file
@@ -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
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
private/
|
||||||
|
dist/
|
||||||
|
*.pfx
|
||||||
|
*.snk
|
||||||
|
*.log
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
71
AGENTS.md
71
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`.
|
- Do not add Defender, SmartScreen, UAC, firewall, or policy bypasses.
|
||||||
- If the working tree is clean, run `git pull --ff-only` before editing.
|
- Do not add silent certificate installation.
|
||||||
- If local changes exist, preserve them and do not overwrite user work.
|
- Do not commit `.pfx`, private keys, passwords, tokens, or signing secrets.
|
||||||
- Conserve context tokens: use `rg`, targeted file reads, and short summaries instead of loading unrelated files or long logs.
|
- 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:
|
- `scripts/` contains the PowerShell implementation.
|
||||||
|
- `assets/certificates/` contains public certificates only.
|
||||||
- `files/` contains templates copied into target repositories.
|
- `private/` is ignored and may contain local signing material.
|
||||||
- `agent-quickstart.md`, `new-repository.md`, and `existing-project.md` are agent workflows.
|
- `docs/integration-prompt.md` is the prompt for adding MrTrust to other projects.
|
||||||
- `manifest.json` is the source of truth for copy targets and placeholders.
|
- `docs/security-model.md` documents the intended behavior and limits.
|
||||||
- `profiles/` contains stack-specific guidance.
|
- `MrTrust.ps1 gui` is the user-facing GUI entry point.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
Before committing:
|
Before finishing changes, run:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-Content manifest.json | ConvertFrom-Json | Out-Null
|
$scripts = Get-ChildItem .\scripts -Filter *.ps1
|
||||||
Get-Content manifest.schema.json | ConvertFrom-Json | Out-Null
|
foreach ($script in $scripts) {
|
||||||
Get-Content files\blueprint.json | ConvertFrom-Json | Out-Null
|
$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
|
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.
|
|
||||||
|
|||||||
44
CHANGELOG.md
44
CHANGELOG.md
@@ -1,41 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to the Codex Agent Repository Kit are documented here.
|
## 0.1.0
|
||||||
|
|
||||||
## 1.0.5 - 2026-05-15
|
- Added MrTrust certificate generation, installation, removal, and signing scripts.
|
||||||
|
- Added a simple Windows GUI for installing and removing MrTrust.
|
||||||
- Restored the rainbow section divider theme in the human-facing `README.md`.
|
- Added a Windows launcher EXE and Gitea Runner workflow for release ZIP builds.
|
||||||
- Added separate minimal permission guidance for `REGISTRY_TOKEN` and `GITEA_TOKEN`.
|
- Added integration prompt for other Windows projects.
|
||||||
- Clarified where package-only and API-capable tokens should be used.
|
- Added security model documentation.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
52
MrTrust.ps1
Normal file
52
MrTrust.ps1
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
389
README.md
389
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:
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
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.
|
## What It Contains
|
||||||
- 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.
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
- `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.
|
Create the certificates:
|
||||||
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.
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## SSH Setup
|
|
||||||
|
|
||||||
Generate a key if you do not already have one:
|
|
||||||
|
|
||||||
```powershell
|
```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
|
```powershell
|
||||||
Start-Service ssh-agent
|
.\MrTrust.ps1 install
|
||||||
ssh-add $env:USERPROFILE\.ssh\id_ed25519
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Show the public key:
|
Open the GUI:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub
|
.\MrTrust.ps1 gui
|
||||||
```
|
```
|
||||||
|
|
||||||
Add that public key in Gitea:
|
Sign another project build:
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
```
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## 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` |
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## 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`.
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## Setting Local Tokens
|
|
||||||
|
|
||||||
Set a local token for Codex or shell-based API work.
|
|
||||||
|
|
||||||
Current PowerShell session:
|
|
||||||
|
|
||||||
```powershell
|
```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
|
```powershell
|
||||||
setx GITEA_TOKEN "paste-token-here"
|
.\MrTrust.ps1 uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
Open a new terminal after `setx`.
|
Build a user-facing ZIP release:
|
||||||
|
|
||||||
Test repository API access:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
|
.\scripts\New-MrTrustRelease.ps1 -Version 0.1.0
|
||||||
Invoke-RestMethod `
|
|
||||||
-Uri "https://git.wilkensxl.de/api/v1/repos/REPOSITORY_OWNER/REPOSITORY_NAME" `
|
|
||||||
-Headers $headers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```powershell
|
||||||
Invoke-RestMethod `
|
.\MrTrust.ps1 gui
|
||||||
-Uri "https://git.wilkensxl.de/api/v1/repos/REPOSITORY_OWNER/REPOSITORY_NAME/issues?state=open&limit=1" `
|
|
||||||
-Headers $headers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
By default, MrTrust installs trust only for the current Windows user:
|
||||||
|
|
||||||
## Setting Repository Secrets
|
|
||||||
|
|
||||||
In Gitea:
|
|
||||||
|
|
||||||
```text
|
```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
|
```powershell
|
||||||
REGISTRY_TOKEN
|
.\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
|
Yes. Give another agent this repository URL and the target Windows project, then paste `docs/integration-prompt.md`.
|
||||||
GITEA_TOKEN
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep package publishing and release or issue automation separate when possible. It makes permission reviews easier.
|
Both sides have to be wired:
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
- 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,
|
## Recommended Project Integration
|
||||||
- copies them to URL-safe filenames,
|
|
||||||
- uploads immutable versioned packages,
|
|
||||||
- updates a stable `latest` package path.
|
|
||||||
|
|
||||||
The workflow uses:
|
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.
|
||||||
|
|
||||||
```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`.
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
```
|
|
||||||
|
|||||||
BIN
assets/certificates/MrSphay-CodeSigning.cer
Normal file
BIN
assets/certificates/MrSphay-CodeSigning.cer
Normal file
Binary file not shown.
BIN
assets/certificates/MrSphay-LocalTrust-Root.cer
Normal file
BIN
assets/certificates/MrSphay-LocalTrust-Root.cer
Normal file
Binary file not shown.
15
assets/certificates/README.md
Normal file
15
assets/certificates/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Public Certificates
|
||||||
|
|
||||||
|
Place the public MrSphay trust certificate here:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MrSphay-LocalTrust-Root.cer
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate it with:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\New-MrTrustCertificate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Only public `.cer` files belong in this directory. Never place `.pfx` files, private keys, or passwords here.
|
||||||
7
assets/certificates/thumbprints.txt
Normal file
7
assets/certificates/thumbprints.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
MrSphay Local Trust Root
|
||||||
|
Subject: CN=MrSphay Local Trust Root
|
||||||
|
Thumbprint: 39F7458E6E2C1126E93E6A1F228196006B174DF2
|
||||||
|
|
||||||
|
MrSphay Code Signing
|
||||||
|
Subject: CN=MrSphay Code Signing
|
||||||
|
Thumbprint: A024A89200469F099EC4A172B4F96F6428AFD41B
|
||||||
33
docs/integration-prompt.md
Normal file
33
docs/integration-prompt.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# MrTrust Integration Prompt
|
||||||
|
|
||||||
|
Use this prompt in another Windows project when you want Codex or another agent to add MrTrust support.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Integrate MrTrust support into this Windows project.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
- Users should be able to run MrTrust once to trust software from MrSphay on their own PC.
|
||||||
|
- The project must not disable Defender, SmartScreen, UAC, or Windows security policy.
|
||||||
|
- All trust changes must be visible and require explicit user confirmation.
|
||||||
|
|
||||||
|
Implementation requirements:
|
||||||
|
- Add a docs section that explains MrTrust and links to or bundles the MrTrust release ZIP.
|
||||||
|
- Add a build or release signing step that signs Windows artifacts with the MrSphay code-signing certificate.
|
||||||
|
- Do not commit any .pfx file, private key, certificate password, or signing secret.
|
||||||
|
- If the project has an installer, add an optional "Open MrTrust" action that runs:
|
||||||
|
powershell.exe -ExecutionPolicy Bypass -File .\MrTrust\MrTrust.ps1 gui
|
||||||
|
- Prefer CurrentUser certificate installation by default.
|
||||||
|
- Only offer LocalMachine installation when the user explicitly chooses an all-users install and the process is elevated.
|
||||||
|
- Add an uninstall path or documentation that runs:
|
||||||
|
powershell.exe -ExecutionPolicy Bypass -File .\MrTrust\MrTrust.ps1 uninstall
|
||||||
|
- Keep the UI wording clear: the user is trusting MrSphay signed software, not bypassing Windows security.
|
||||||
|
- If this project produces an .exe, .msi, .dll, .ps1, .psm1, .psd1, or .cat release artifact, sign it with:
|
||||||
|
powershell.exe -ExecutionPolicy Bypass -File .\MrTrust\MrTrust.ps1 sign -Path <artifact-path> -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.
|
||||||
|
```
|
||||||
40
docs/security-model.md
Normal file
40
docs/security-model.md
Normal file
@@ -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.
|
||||||
50
scripts/Build-MrTrustExe.ps1
Normal file
50
scripts/Build-MrTrustExe.ps1
Normal file
@@ -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"
|
||||||
90
scripts/Install-MrTrust.ps1
Normal file
90
scripts/Install-MrTrust.ps1
Normal file
@@ -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"
|
||||||
90
scripts/New-MrTrustCertificate.ps1
Normal file
90
scripts/New-MrTrustCertificate.ps1
Normal file
@@ -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."
|
||||||
|
}
|
||||||
70
scripts/New-MrTrustRelease.ps1
Normal file
70
scripts/New-MrTrustRelease.ps1
Normal file
@@ -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"
|
||||||
104
scripts/Sign-MrTrustProject.ps1
Normal file
104
scripts/Sign-MrTrustProject.ps1
Normal file
@@ -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)."
|
||||||
324
scripts/Start-MrTrustGui.ps1
Normal file
324
scripts/Start-MrTrustGui.ps1
Normal file
@@ -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)
|
||||||
87
scripts/Uninstall-MrTrust.ps1
Normal file
87
scripts/Uninstall-MrTrust.ps1
Normal file
@@ -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."
|
||||||
58
src/MrTrustLauncher.cs
Normal file
58
src/MrTrustLauncher.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user