This document covers tasks that require maintainer access: key management, releases, code signing, and other operational procedures. It is not intended for general contributors.
- RSA License Key Setup
- Key Rotation
- Cutting a Release
- Velopack Auto-Update Setup
- Code Signing Setup
- Cross-Org PAT — ShackDesk-Site Dispatch
- NuGet and Actions Dependency Updates
PortPane uses RSA-2048 / SHA-256 / PKCS1 to verify commercial license keys offline. The private key signs license keys. The public key is embedded in the app to verify them.
Run this PowerShell script on a secure machine. Do not run it in a CI environment.
# Generate RSA-2048 key pair in .NET XML format
$rsa = [System.Security.Cryptography.RSA]::Create(2048)
# Private key — contains all parameters — KEEP SECRET
$privateKeyXml = $rsa.ToXmlString($true)
$privateKeyXml | Out-File -FilePath "portpane-private.xml" -Encoding UTF8 -NoNewline
# Public key — modulus and exponent only — safe to embed in source
$publicKeyXml = $rsa.ToXmlString($false)
$publicKeyXml | Out-File -FilePath "portpane-public.xml" -Encoding UTF8 -NoNewline
$rsa.Dispose()
Write-Host "Done."
Write-Host "Private key -> portpane-private.xml (store securely, then delete this file)"
Write-Host "Public key -> portpane-public.xml (embed in source, copy to keys/)"IMPORTANT: Delete
portpane-private.xmlfrom your local machine after completing the steps below. Never commit it. Never email it. Store it only in GitHub Secrets and a secure offline backup.
- Open the repository on GitHub.
- Go to Settings → Secrets and variables → Actions.
- Click New repository secret.
- Name:
RSA_LICENSE_PRIVATE_KEY - Value: paste the entire contents of
portpane-private.xml(single-line XML is fine —RSA.FromXmlString()does not require line breaks or indentation). - Click Add secret.
The secret is used only by the license key generator tool — not by the app build itself.
-
Open
src/PortPane/Services/LicenseService.cs. -
Find the
PublicKeyXmlconstant (search for<RSAKeyValue>). -
Replace the existing value with the contents of
portpane-public.xml. Format it as a single concatenated string:private const string PublicKeyXml = "<RSAKeyValue>" + "<Modulus>...your modulus here...</Modulus>" + "<Exponent>AQAB</Exponent>" + "</RSAKeyValue>";
-
Copy
portpane-public.xmltokeys/portpane-public.pem(update the file content, keeping the header comment). This is the reference copy checked into the repo. -
Commit with a message like
Update RSA public key.
The payload signed by the private key is a pipe-delimited string in this exact order:
portpane|{type}|{licensee}|{email}|{issued}|{expires}
| Field | Value |
|---|---|
portpane |
Literal string — always "portpane" |
type |
personal, club, or emcomm |
licensee |
Full name of the licensee |
email |
Email address of the licensee |
issued |
ISO 8601 UTC string, e.g. 2026-03-27T00:00:00Z |
expires |
ISO 8601 UTC string, or the literal string never |
Use empty string for any null/missing field. Values must match the JSON exactly.
License key generator snippet (C#):
using var rsa = RSA.Create();
rsa.FromXmlString(privateKeyXml); // from RSA_LICENSE_PRIVATE_KEY secret
string payload = string.Join("|",
"portpane", type, licensee, email, issued, expires);
byte[] data = Encoding.UTF8.GetBytes(payload);
byte[] sig = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
string sigB64 = Convert.ToBase64String(sig);
var licenseJson = JsonSerializer.Serialize(new {
app = "portpane",
type = type,
licensee = licensee,
email = email,
issued = issued,
expires = expires,
version_max = "1.x",
signature = sigB64
});
string licenseKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(licenseJson));
// licenseKey is what you deliver to the customerRSA keys do not expire on their own. Rotate the key pair if:
- The private key is ever suspected to be compromised.
- The GitHub repository or secrets are accessed by an unauthorized party.
- You choose a scheduled rotation policy (e.g. every 2 years).
Rotation steps:
- Generate a new key pair following Section 1.
- Store the new private key in the
RSA_LICENSE_PRIVATE_KEYGitHub secret (overwrite the existing secret). - Embed the new public key in
LicenseService.csandkeys/portpane-public.pem. - Re-issue license keys for all current commercial licensees signed with the new key. Keys signed with the old key will no longer validate after the update is shipped.
- Notify affected licensees before releasing the update so they can request new keys.
- Commit and tag a new release.
After rotation, any existing commercial license key becomes invalid. Plan accordingly.
| File | What to change |
|---|---|
src/PortPane/BrandingInfo.cs |
Version constant |
src/PortPane/PortPane.csproj |
<Version>, <FileVersion>, <AssemblyVersion> |
installer/PortPane.iss |
#define AppVersion |
CHANGELOG.md |
Move [Unreleased] items to a new [x.y.z] — YYYY-MM-DD section |
# 1. Make sure main is clean and all changes are committed
git status
# 2. Update the four files listed above, then commit
git add src/PortPane/BrandingInfo.cs src/PortPane/PortPane.csproj installer/PortPane.iss CHANGELOG.md
git commit -m "Release v0.6.0-beta"
# 3. Create and push a version tag — this triggers the full CI release pipeline
git tag v0.6.0-beta
git push origin main
git push origin v0.6.0-beta- build-and-test — builds Debug, runs xUnit tests
- publish — builds self-contained
PortPane.exe, computes SHA-256 - installer — builds Inno Setup installer (if ISCC.exe is available on runner)
- release — creates a GitHub Release with the exe, installer, and CHANGELOG notes
The -beta or -alpha suffix in the tag name automatically marks the GitHub Release
as a pre-release.
MAJOR.MINOR.PATCH[-suffix]
Examples:
0.5.0-beta Pre-release beta
0.5.1-beta Beta patch (bug fixes only)
1.0.0 First stable release
1.1.0 Minor feature addition
1.1.1 Patch / bug fix
The app checks BrandingInfo.UpdateEndpoint for updates at most once every 24 hours.
Host a latest.json file at the UpdateEndpoint URL. Velopack's expected format:
{
"version": "0.6.0-beta",
"url": "https://shackdesk.com/releases/PortPane-0.6.0-beta-win-x64.exe",
"sha256": "abc123...",
"releaseNotes": "See https://github.com/Computer-Tsu/shackdesk-portpane/releases"
}Update this file each time a new release is published. The CI publish job outputs
the SHA-256 hash as a step output and artifact (PortPane.exe.sha256).
The endpoint is defined in BrandingInfo.cs:
public const string UpdateEndpoint = "https://shackdesk.com/update/latest.json";Change this before the first stable release if the hosting URL differs.
Code signing is currently a placeholder in the CI pipeline. When you obtain an EV or OV certificate:
Uncomment and configure the signing step (currently lines ~117–127):
- name: Sign executable
run: >
signtool.exe sign
/fd sha256
/tr http://timestamp.digicert.com
/td sha256
/f certificate.pfx
/p ${{ secrets.SIGNING_PASSWORD }}
./publish/PortPane.exeAdd SIGNING_PASSWORD (the PFX password) as a GitHub Actions secret.
Uncomment and configure the SignTool line:
SignTool=standard sign /fd sha256 /tr http://timestamp.digicert.com /td sha256 $fThe build.yml also has a placeholder for ConfuserEx obfuscation (lines ~105–115).
To enable it, create PortPane.crproj targeting ./publish/PortPane.exe and update
the build step to call crconf.exe.
The SITE_DISPATCH_PAT GitHub Actions secret authorises the PortPane release job to trigger
the update JSON workflow in Computer-Consultant/ShackDesk-Site after each release.
Token scope:
- Resource owner:
Computer-Consultant - Repository:
ShackDesk-Siteonly - Permissions:
Contents: Read and write,Actions: Read and write
Expiry: ~1 year from creation. Renewal reminder: issue #14
To renew:
- GitHub → Personal account → Settings → Developer settings → Personal access tokens → Fine-grained tokens
- Find the token scoped to
Computer-Consultant/ShackDesk-Siteand regenerate it Computer-Tsu/ShackDesk-PortPane→ Settings → Secrets → Actions → updateSITE_DISPATCH_PAT- Verify the next release triggers the update JSON workflow successfully
If the token expires: PortPane releases still build and ship normally — only the automatic
update of stable.json / beta.json / alpha.json in ShackDesk-Site will fail silently.
Manually update the JSON files in ShackDesk-Site to restore auto-update functionality.
Dependabot opens pull requests automatically every Monday for:
- NuGet packages (
nugetecosystem) - GitHub Actions versions (
github-actionsecosystem)
PRs are labelled dependencies and automated.
Review checklist for Dependabot PRs:
- Check the package changelog for breaking changes before merging.
- For
Velopackupdates: verify the update/packaging API hasn't changed. - For
NAudioupdates: spot-check audio device enumeration still works. - For Actions updates (
actions/checkout,actions/setup-dotnet, etc.): verify the major version hasn't changed (e.g.@v4→@v5requires manual review).
To update manually:
# Check for outdated packages
dotnet list src/PortPane/PortPane.csproj package --outdated
# Update a specific package
dotnet add src/PortPane/PortPane.csproj package Serilog --version 4.2.0For security vulnerability reporting, see SECURITY.md. For contribution guidelines, see CONTRIBUTING.md.