Skip to content

Fix cert generate --manifest silently falling back to user name (#839) - #890

Merged
Nikola Metulev (nmetulev) merged 6 commits into
mainfrom
zateutsch-cert-manifest-publisher-fallback
Sep 21, 2026
Merged

Nikola Metulev (nmetulev) merged 6 commits into
mainfrom
zateutsch-cert-manifest-publisher-fallback

Conversation

@zateutsch

Copy link
Copy Markdown
Contributor

Summary

Fixes #839.

winapp cert generate --manifest <path> silently substituted the OS user name as the certificate publisher when it could not extract Identity/@Publisher from the manifest — exiting 0 and reporting success. Because the publisher can never match Identity/@Publisher, the resulting certificate produces a package that fails to install, while the CLI reports success.

This happened for partially-complete manifests (valid Identity/@Publisher, but missing <Applications>), which are common mid-development — exactly when a dev certificate gets generated. The cause: manifest parsing (ParseAppxManifestFromPathAsync) required Name + Publisher + ApplicationId together, so it threw even though the publisher was present, and the caller swallowed the error and fell back to the user name.

What changed

  • MsixService.ExtractPublisherFromPathAsync — a publisher-only extractor that reads just Identity/@Publisher and rejects a missing/empty value. It does not require an <Applications> element or package Name, so partial manifests work.
  • CertGenerateCommand — when --manifest is named explicitly and no --publisher overrides it, the publisher is resolved up front. If it can't be extracted, the command fails with a clear, actionable error (non-zero exit) instead of silently substituting the default:
    • text mode: Could not extract the publisher from the manifest '<path>': <reason>. Fix the manifest's Identity Publisher attribute, or pass --publisher explicitly.
    • --json mode: a single {"error": "..."} document on stdout.
    • Auto-discovered project manifests (no explicit --manifest) stay lenient and still fall back, which is the appropriate behavior when no source was explicitly requested.
  • docs/usage.md — the --manifest bullet now describes the fail-loud behavior.

The user-name fallback remains reasonable when no publisher source was given at all; it is only removed for the case where the caller explicitly named a manifest.

Behavior

Input Before After
--manifest with valid publisher, no <Applications> Silently used OS user name, exit 0 Uses the manifest publisher, exit 0
--manifest with no/empty publisher Silently used OS user name, exit 0 Fails with actionable error, exit 1, no PFX
--manifest … --json, extraction fails exit 0 / wrong publisher Single {"error":...}, exit 1
No --manifest, auto-discovered project manifest fails Falls back to default Falls back to default (unchanged)

Testing

  • dotnet build src/winapp-CLI/winapp.sln -c Debug — succeeds, 0 warnings.
  • New unit tests (MsixServicePackagingTests): nonexistent file, partial manifest returns publisher, missing publisher throws, empty/whitespace publisher throws.
  • New command tests (CertGenerateCommandTests): explicit manifest missing publisher (text + --json, asserting a single JSON document), partial manifest succeeds with the correct subject.
  • Verified end-to-end against the exact repro from cert generate --manifest silently falls back to the OS user name when publisher extraction fails #839.

No command/option surface changed, so docs/cli-schema.json and the npm command types are unaffected.

`winapp cert generate --manifest <path>` silently substituted the OS user
name as the certificate publisher when it could not extract
`Identity/@Publisher`, exiting 0 and reporting success. The resulting
certificate can never match the manifest, so the signed package fails to
install. This commonly happened with partially-complete manifests (valid
publisher, but no `<Applications>` element), because manifest parsing
required Name + Publisher + ApplicationId together.

- Add `MsixService.ExtractPublisherFromPathAsync`, which reads only
  `Identity/@Publisher` (rejecting a missing/empty value) and no longer
  requires an Application element or package Name.
- In `CertGenerateCommand`, when `--manifest` is named explicitly and no
  `--publisher` overrides it, resolve the publisher up front and fail with a
  clear error (non-zero exit; single `{"error":...}` under `--json`) instead
  of falling back to the default. Auto-discovered project manifests stay
  lenient.
- Document the behavior in docs/usage.md and add unit + command tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 19, 2026 21:22
Comment thread src/winapp-CLI/WinApp.Cli/Commands/CertGenerateCommand.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/CertGenerateCommandTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/CertGenerateCommandTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/CertGenerateCommandTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/CertGenerateCommandTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/CertGenerateCommandTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/CertGenerateCommandTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/MsixServicePackagingTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/MsixServicePackagingTests.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/MsixServicePackagingTests.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The new extraction catch misreports command cancellation as invalid manifest input.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes certificate publisher inference for incomplete manifests and fails clearly when explicit manifest extraction fails.

Changes:

  • Adds publisher-only manifest extraction.
  • Enforces text and JSON error behavior.
  • Adds regression tests and documentation.
File Description
MsixService.cs Adds publisher-only extraction.
CertGenerateCommand.cs Validates explicit manifests before generation.
MsixServicePackagingTests.cs Tests extraction edge cases.
CertGenerateCommandTests.cs Tests command success and failure paths.
docs/​usage.md Documents explicit-manifest behavior.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/winapp-CLI/WinApp.Cli/Commands/CertGenerateCommand.cs Outdated
- Exclude OperationCanceledException from the manifest-publisher extraction
  catch in CertGenerateCommand so a cancellation mid-read is not misreported
  as a "Could not extract the publisher" input error (matches the repo's
  established `when (ex is not OperationCanceledException)` pattern).
- Add a regression test asserting a canceled cert generate --manifest does not
  report the manifest error and creates no PFX.
- Switch the new tests' path construction from Path.Combine to Path.Join.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

⏳ Build in progress — metrics below are from a previous commit and will update when the current build finishes.

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 57.03 MB 57.03 MB 📈 +5.0 KB (+0.01%)
CLI (x64) 57.07 MB 57.07 MB 📈 +5.0 KB (+0.01%)
MSIX (ARM64) 23.72 MB 23.72 MB 📉 -0.5 KB (-0.00%)
MSIX (x64) 25.16 MB 25.16 MB 📈 +0.5 KB (+0.00%)
NPM Package 49.44 MB 49.45 MB 📈 +7.6 KB (+0.01%)
NuGet Package 49.55 MB 49.55 MB 📈 +6.5 KB (+0.01%)

Test Results

✅ 7606 passed, 37 skipped out of 7643 tests in 1182.2s (+8 tests, -10.1s vs. baseline)

Test Coverage

✅ 85.7% line coverage, 79.9% branch coverage · ✅ no change vs. baseline

CLI Startup Time

65ms median (x64, winapp --version) · ✅ -7ms vs. baseline

Try This Build

Installs the MSIX for your architecture, replacing any previously installed build. Needs the GitHub CLI — the command offers to install it and sign you in if it is missing.

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) 890
Switching between builds often?

Put the tool on your PATH once:

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPath

Then this build is just:

winapp-pr 890

Run winapp-pr with no arguments to pick from a list of open PRs.


Updated 2026-09-19 22:30:38 UTC · commit 2d115f7 · workflow run

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The solution builds cleanly, targeted tests pass, and published CLI validation confirms the intended success and failure paths.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@nmetulev Nikola Metulev (nmetulev) added this to the 0.7 milestone Sep 21, 2026
@nmetulev
Nikola Metulev (nmetulev) merged commit e9ab2ea into main Sep 21, 2026
10 checks passed
@nmetulev
Nikola Metulev (nmetulev) deleted the zateutsch-cert-manifest-publisher-fallback branch September 21, 2026 20:47
Nikola Metulev (nmetulev) added a commit that referenced this pull request Sep 21, 2026
## Description

`winapp cert generate --publisher` (and `manifest generate
--publisher-name`, which share the same normalization) accepted
malformed X.500 distinguished names (DNs) and **silently generated a
certificate or manifest that can never match the MSIX
`Identity/@Publisher`** — exit 0, a `.pfx` on disk, and a failure that
only surfaces much later as an unexplained signing/install error.
Verified against issue #845.

`PublisherDnHelper` now validates the publisher up front and rejects,
with a message naming the exact problem, any value the MSIX manifest
publisher cannot faithfully represent:

| `--publisher` input | Old behavior | New behavior |
|---|---|---|
| `CN=` | `CN=""` (empty CN) | exit 1 — names the empty `CN` |
| `CN=A, O=` | empty `O` | exit 1 — names the empty `O` |
| `=Contoso` / `CN=A,,O=B` | wrapped as a literal CN | exit 1 — not a
valid DN |
| `OID.2.5.4.3=A,,O=B` | wrapped as a literal CN | exit 1 — not a valid
DN |
| `CN=A+O=B` (multi-valued RDN) | empty value could hide | exit 1 —
multi-valued RDN unsupported |
| `CN=A;O=B` (semicolon separator) | cert used commas, manifest kept `;`
| exit 1 — use comma separators |
| `CN=A,` / `CN=A+` (trailing separator) | cert dropped it, manifest
kept it | exit 1 — remove the trailing separator |
| `CN=Contoso\Bar` (backslash) | unmatchable in the manifest | exit 1 —
backslash unsupported |
| `""` (explicit empty) | silently used the default publisher | exit 1 —
publisher cannot be empty |

Valid input is unaffected and round-trips as before: `CN=Contoso Inc,
O=Contoso, C=US`, bare names like `Contoso` (wrapped as `CN=Contoso`),
bare names that merely contain `=` (e.g. `R&D = Team`), and quoted data
such as `CN="A,"`. A new non-throwing `TryNormalize` lets the command
pre-validate an explicit `--publisher` and fail cleanly — emitting a
JSON error envelope under `--json` — instead of falling through to a
default. This complements #890's `--manifest` extraction (merged from
main): `--manifest` still supplies the publisher when no explicit
`--publisher` is given, and an explicit `--publisher` is validated even
alongside `--manifest`.

## Usage Example

```console
$ winapp cert generate --publisher "CN=A,,O=B"
❌ Publisher 'CN=A,,O=B' is not a valid distinguished name (DN). Use a plain name (wrapped as CN=<name>) or a valid DN such as 'CN=Contoso, O=Contoso, C=US'.
$ echo $LASTEXITCODE
1

$ winapp cert generate --publisher "CN=Contoso, O=Contoso, C=US"
✅ Development certificate generated successfully.
```

## Related Issue

Fixes #845.

## Type of Change

- 🐛 Bug fix

## Checklist

- [x] New tests added for new functionality (if applicable)
- [x] Tested locally on Windows
- [x] [docs/usage.md](../docs/usage.md) updated (if CLI commands
changed)
- [x] Shipped skills updated in `plugins/winapp/skills/` (if CLI
commands/workflows changed)

## Additional Notes

Documentation was aligned to the narrowed contract on every surface that
promised "any valid X.500 DN": `docs/usage.md`, the `cert generate` /
`manifest generate` option descriptions (and the regenerated
`docs/cli-schema.json`),
`plugins/winapp/com.github.copilot/agents/winapp.agent.md`, and
`plugins/winapp/skills/winapp-signing/SKILL.md`.

Local validation used the Debug CLI: the NativeAOT-published binary
can't be built in this environment (missing VS native linker), and these
are argument-parsing/validation-logic changes that aren't AOT-sensitive.
The pipeline's `build-and-package` (including NativeAOT publish, full
test shards, docs validation, and sample builds) is green on the
pre-merge head; CI re-runs on the merge commit.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Nikola Metulev <nmetulev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cert generate --manifest silently falls back to the OS user name when publisher extraction fails

3 participants