From 934ecfc6a837b894c16aa790a082b9223452315b Mon Sep 17 00:00:00 2001 From: Alpaq92 Date: Mon, 6 Jul 2026 21:54:47 +0200 Subject: [PATCH 1/4] fix(demo): theme the xterm viewport background to remove the black bar in light mode xterm.css hardcodes background-color: #000 on .xterm-viewport and never re-syncs it to the theme passed into the Terminal constructor. The terminal's text rows only cover a whole number of cell rows, so the strip between the last row and the container's bottom edge showed that un-themed black viewport through as a black bar in light mode. #terminal .xterm-viewport already themed the scrollbar; it now also sets background-color: var(--bg) so the viewport follows the page theme in both modes. Co-Authored-By: Claude Sonnet 5 --- TopSecret.Demo.Wasm/wwwroot/index.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/TopSecret.Demo.Wasm/wwwroot/index.html b/TopSecret.Demo.Wasm/wwwroot/index.html index ce48cd4..b14a465 100644 --- a/TopSecret.Demo.Wasm/wwwroot/index.html +++ b/TopSecret.Demo.Wasm/wwwroot/index.html @@ -97,9 +97,14 @@ unthemed by default — the browser renders it (track, thumb, and especially the corner where the vertical scrollbar meets the bottom edge) in its native light colours regardless of page - theme, showing up as a stray white patch in dark mode. */ + theme, showing up as a stray white patch in dark mode. + xterm.css also hardcodes background-color: #000 on the viewport + and (as of xterm 6) never re-syncs it to the theme, so the + leftover strip below the last full row of cells shows through + as a black bar in light mode. */ #terminal .xterm-viewport { scrollbar-color: var(--border) var(--bg); + background-color: var(--bg); } #terminal .xterm-viewport::-webkit-scrollbar { width: 10px; From e80f18499910f788300051d6f27d54521593d869 Mon Sep 17 00:00:00 2001 From: Alpaq92 Date: Mon, 6 Jul 2026 21:55:11 +0200 Subject: [PATCH 2/4] fix(security): guard against 32-bit overflow when sizing ciphertext buffers EncryptInternal computed byteLen = plain.Length * 2 unchecked, while the build-buffer growth path a few hundred lines up already wraps the identical doubling in checked(...). A plaintext at or beyond int.MaxValue / 2 UTF-16 chars would overflow byteLen to a negative or too-small value, under-allocating the buffer the encryption then writes into. Wrapped the multiplication in checked(...) to match the existing convention, so construction now throws OverflowException instead of risking a truncated allocation. README: documented the new threshold and behavior in the Memory footprint section. Co-Authored-By: Claude Sonnet 5 --- README.md | 2 ++ TopSecret.ProtectedString/ProtectedString.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d7fc45d..2aff1ad 100644 --- a/README.md +++ b/README.md @@ -794,6 +794,8 @@ The fork's fix (v2.0.0) runs the first lane inline on the calling thread and dis The 32-byte process master key plus its protector live once per process, regardless of how many `ProtectedString`s you allocate. Memory locking has a per-process budget on libc targets — see [Memory-locking policy](#memory-locking-policy) for the budget number and the failure-behaviour knob. +Content length is checked against 32-bit overflow before encryption: a plaintext at or beyond `int.MaxValue / 2` UTF-16 chars (~1.07 billion, since each char becomes 2 ciphertext bytes) throws `OverflowException` instead of silently under-allocating. In practice you'll hit `Array.MaxLength` or run out of process memory well before that threshold. + ## Build & test ``` diff --git a/TopSecret.ProtectedString/ProtectedString.cs b/TopSecret.ProtectedString/ProtectedString.cs index 7ea9332..5c9366d 100644 --- a/TopSecret.ProtectedString/ProtectedString.cs +++ b/TopSecret.ProtectedString/ProtectedString.cs @@ -1294,7 +1294,7 @@ private void EncryptInternal(ReadOnlySpan plain) // change which key we encrypt under mid-operation. Debug.Assert(_instanceProtector is not null, "InitInstance must run before EncryptInternal"); - int byteLen = plain.Length * 2; + int byteLen = checked(plain.Length * 2); // Allocate (and lock) the new state up front. If anything in here // throws — e.g., the configured policy is Throw and a lock failed — From 9b9da50c9ab4c0d5bb1f6993c9785b29df522139 Mon Sep 17 00:00:00 2001 From: Alpaq92 Date: Mon, 6 Jul 2026 21:55:25 +0200 Subject: [PATCH 3/4] feat(security): add SECURITY.md, CodeQL scanning, and CI least-privilege hardening - Add SECURITY.md: supported-versions policy, private-vulnerability- reporting instructions (already referenced from CONTRIBUTING.md but never actually written down), response expectations, and an explicit scope section deferring to the README's documented limitations. - Add codeql.yml: CodeQL analysis for the C# surface on push/PR to master plus a weekly schedule, scoped to net10.0 on ubuntu-latest via the same CI_TFMS mechanism ci.yml's ubuntu leg already uses (no extra workloads needed). - ci.yml and build-platform-tfms.yml previously had no explicit permissions: block, so they ran under whatever the repo/org default GITHUB_TOKEN scope happened to be. Both only build/test, so they now explicitly declare contents: read. - Pin every third-party Action (actions/checkout, actions/setup-dotnet, actions/upload-pages-artifact, actions/deploy-pages, dependabot/fetch-metadata, googleapis/release-please-action) to the commit SHA the currently-pinned tag resolves to, with a trailing "# vN" comment for readability. Tags are mutable; a compromised action maintainer could re-point one without Dependabot noticing until the next real release. Behavior is unchanged - same version, just an immutable reference. Dependabot already tracks the github-actions ecosystem, so it can still propose SHA bumps going forward. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/build-platform-tfms.yml | 15 +++--- .github/workflows/ci.yml | 7 ++- .github/workflows/codeql.yml | 55 +++++++++++++++++++++ .github/workflows/dependabot-auto-merge.yml | 2 +- .github/workflows/pages.yml | 8 +-- .github/workflows/release.yml | 6 +-- SECURITY.md | 47 ++++++++++++++++++ 7 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/codeql.yml create mode 100644 SECURITY.md diff --git a/.github/workflows/build-platform-tfms.yml b/.github/workflows/build-platform-tfms.yml index bb10fbb..9121560 100644 --- a/.github/workflows/build-platform-tfms.yml +++ b/.github/workflows/build-platform-tfms.yml @@ -21,6 +21,9 @@ name: Build platform-specific TFMs on: workflow_dispatch: +permissions: + contents: read + jobs: ios: name: Build net10.0-ios @@ -29,10 +32,10 @@ jobs: runs-on: macos-14 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 - name: Install ios workload run: dotnet workload install ios @@ -47,10 +50,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 - name: Install android workload run: dotnet workload install android @@ -68,10 +71,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 - name: Install wasm-tools workload run: dotnet workload install wasm-tools diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f2bfc2..42ccfbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: push: branches: [master] +permissions: + contents: read + # One CI run per ref at a time; a push flurry cancels superseded runs # instead of stacking them. (Once, 18 concurrently-hung runs exhausted the # account's ~20 concurrent-job quota and starved every other workflow.) @@ -51,7 +54,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # global.json pins the SDK floor at 10.0.300 (with # `rollForward: latestFeature`, so any 10.0.3xx-or-newer SDK @@ -63,7 +66,7 @@ jobs: # global.json when no version is given, installing the requested # floor on the runner if it isn't already present. - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 # GitHub-hosted runners exist for Windows, Linux, and macOS but NOT # for iOS, Android, or browser WebAssembly. Those TFMs are diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..04d2488 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,55 @@ +name: CodeQL + +# Static analysis for the C# surface of this repo — runs on every push/PR to +# master plus a weekly schedule, so newly-added CodeQL query patterns get a +# chance to flag existing code, not just new diffs. +# +# Scoped to net10.0 on ubuntu-latest via CI_TFMS (the same mechanism +# ci.yml's ubuntu leg uses): CodeQL needs one successful build to extract +# from, and the platform TFMs (ios/android/macos/browser) need workloads +# and runners this leg doesn't have — build-platform-tfms.yml and +# release.yml already compile-check those separately. + +on: + push: + branches: [master] + pull_request: + branches: [master] + schedule: + - cron: '24 3 * * 1' # weekly, Monday 03:24 UTC + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + security-events: write + +jobs: + analyze: + name: Analyze (C#) + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + CI_TFMS: net10.0 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Setup .NET + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 + + - name: Initialize CodeQL + uses: github/codeql-action/init@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3 + with: + languages: csharp + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build TopSecret.ProtectedString.sln --configuration Release --no-restore + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3 diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 24cd6dd..64e156a 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Fetch Dependabot metadata id: meta - uses: dependabot/fetch-metadata@v3 + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 9e9b01b..baf750c 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -41,11 +41,11 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # Honours global.json (SDK floor 10.0.300, rollForward latestFeature). - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 - name: Install wasm-tools workload run: dotnet workload install wasm-tools @@ -78,10 +78,10 @@ jobs: echo "bundle=$bundle" >> "$GITHUB_ENV" - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v5 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: ${{ env.bundle }} - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81afd85..600d0d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: tag: ${{ steps.rp.outputs.tag_name }} steps: - id: rp - uses: googleapis/release-please-action@v5 + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5 with: config-file: release-please-config.json manifest-file: .release-please-manifest.json @@ -81,12 +81,12 @@ jobs: echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: ref: ${{ steps.tag.outputs.name }} - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 # macos-14 is the only hosted runner where all five installs succeed — # required for the main library's six-TFM pack. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..daea2de --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,47 @@ +# Security Policy + +## Supported versions + +Only the latest published version of each package (currently `2.x`) receives +security fixes. There is no LTS branch — given the release cadence, upgrading +is expected to be the fix. + +## Reporting a vulnerability + +Please **do not open a public GitHub issue** for a suspected vulnerability — +in the memory-protection guarantees (secrets recoverable from a memory or +core dump, a key-wrapping tier not doing what it claims, a timing +side-channel in a comparison, a wipe that doesn't happen on some exit path) +or in the supply chain (a CI/release workflow that could be tricked into +running untrusted code or exfiltrating secrets). + +Instead, use GitHub's private vulnerability reporting for this repository: +[Report a vulnerability](https://github.com/Alpaq92/TopSecret.ProtectedString/security/advisories/new). + +Include, where you can: + +- The affected package(s) and version. +- A description of the weakness and, if possible, a minimal repro. +- The platform/TFM it applies to, if platform-specific — this library's + security guarantees differ by key-at-rest tier; see the README's + [Security model](README.md#security-model) section. + +## Response + +This is a solo-maintained project with no formal SLA. Reports are triaged +and acknowledged as soon as reasonably possible, and a confirmed +vulnerability is prioritized over new feature work. + +## Scope + +**In scope:** the packages in this repository (`TopSecret.ProtectedString`, +`TopSecret.ProtectedBlob`, the TPM/Configuration satellites, and the Roslyn +analyzer) and the GitHub Actions workflows that build, test, and release +them. + +**Out of scope:** the demo apps (`TopSecret.Demo`, `TopSecret.Demo.Wasm`) +except where a demo bug points at a real library issue, and anything already +documented as a known, deliberate limitation in the README's +["What this library does **not** do"](README.md#what-this-library-does-not-do-and-why) +section — those are accepted trade-offs, not vulnerabilities, unless you can +show the trade-off is worse in practice than documented. From 7244aaa4ae1840e3c846252f29aba768289c7ef2 Mon Sep 17 00:00:00 2001 From: Alpaq92 Date: Mon, 6 Jul 2026 22:06:12 +0200 Subject: [PATCH 4/4] fix(security): address CodeQL workflow review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set persist-credentials: false on the checkout step — this job never needs to push, so there's no reason to leave the token in the local git config for the rest of the job. - Scope security-events: write (and contents: read) to the analyze job itself instead of the workflow-level default. Note: job-level permissions fully replace the workflow-level block rather than merging with it, so contents: read had to move down too, not just the security-events grant, or the job would have silently lost read access. - Bump github/codeql-action from v3 to v4 (the current major per upstream's own README) on both the init and analyze steps, pinned to the new v4 tag's resolved commit SHA. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/codeql.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 04d2488..92acc3a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,26 +22,30 @@ concurrency: group: codeql-${{ github.ref }} cancel-in-progress: true -permissions: - contents: read - security-events: write - jobs: analyze: name: Analyze (C#) runs-on: ubuntu-latest timeout-minutes: 20 + # Job-level permissions replace (not merge with) any workflow-level + # block, so both grants live here even though only one job exists — + # scoping them to the job that actually needs them, not the workflow. + permissions: + contents: read + security-events: write env: CI_TFMS: net10.0 steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false - name: Setup .NET uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 - name: Initialize CodeQL - uses: github/codeql-action/init@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3 + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 with: languages: csharp @@ -52,4 +56,4 @@ jobs: run: dotnet build TopSecret.ProtectedString.sln --configuration Release --no-restore - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3 + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4