Skip to content

Escape '#' in OPC part names so signature references aren't truncated - #1063

Open
ChatchawanIllyes wants to merge 2 commits into
dotnet:mainfrom
ChatchawanIllyes:fix/opc-part-name-octothorpe
Open

Escape '#' in OPC part names so signature references aren't truncated#1063
ChatchawanIllyes wants to merge 2 commits into
dotnet:mainfrom
ChatchawanIllyes:fix/opc-part-name-octothorpe

Conversation

@ChatchawanIllyes

Copy link
Copy Markdown

Fixes #998.

Root cause

OpcPart builds its Uri directly from the raw zip entry name:

Uri = new Uri(OpcPackage.BasePackageUri, path);

System.Uri treats an unescaped # as introducing a URI fragment. So a content file named e.g. ab#c.txt gets silently split into path ab + fragment c.txt the moment the part is constructed — corrupting everything derived from that Uri: the signature manifest's Reference/@URI, relationship Target paths, and part identity/equality (OpcPart.Equals/GetHashCode).

Escaping the raw path before constructing the Uri fixes the parsing side, but UriHelpers.ToQualifiedPath() — used to serialize the Uri back into the Reference/@URI and relationship Target XML attributes — used UriFormat.Unescaped, which decodes the escape straight back into a literal # when building the string. So the escape alone doesn't survive into the signed XML; the same truncation would reappear at serialization time. This matches what the issue reporter described: a first attempt to percent-encode # didn't fix verification, because something downstream was still emitting an unescaped value.

Fix

  • OpcPart.cs: escape the raw path (UriHelpers.EscapePartPath, new helper) before constructing both the part's Uri and its relationship-file location Uri.
  • UriHelpers.cs: ToQualifiedPath() now uses UriFormat.UriEscaped instead of UriFormat.Unescaped, so # stays encoded as %23 in the Reference/@URI and relationship Target values it produces. ToPackagePath() is untouched — it's used for actual zip-entry lookups and must keep returning the raw, unescaped path to match real entry names.

Testing

Added ShouldSignPartWithOctothorpeInName to OpcPackageSigningTests: signs a package containing a part named ab#c.txt, parses the resulting signature XML, and asserts the Reference/@URI contains %23 (not a raw #) and that its DigestValue matches the SHA-256 of the actual file contents (i.e. the reference resolves to the right part, not a truncated one).

Disclosure on verification: I was unable to execute this repo's test suite locally. Directory.Build.props pins RuntimeIdentifier=win-x64 repo-wide, so the built Sign.Core.dll is a genuine Windows-x64 binary that macOS refuses to load (FileLoadException: The assembly architecture is not compatible with the current process architecture) — not a missing-tool problem, a hard OS/CPU mismatch. Sign.Core.csproj itself does build cleanly (0 warnings/errors) with this change. The new test was written by tracing the fixed code path by hand against System.Uri/UriBuilder documented behavior for UriComponents/UriFormat, and by manually checking every existing case in UriHelpersTests.cs against the changed format flag (none use reserved characters, so none of their assertions change) — but it has not actually been run. Flagging this plainly rather than implying it passed CI locally; happy to iterate on any failures your Windows build turns up.

System.Uri parses an unescaped '#' as introducing a URI fragment. Part
names come straight from raw zip entry names, so a content file like
"ab#c.txt" was silently split into path "ab" plus fragment "c.txt"
when OpcPart built its Uri, corrupting every reference derived from
it (the signature manifest Reference/@uri, relationship Target
paths, and part identity/equality).

Escaping the raw path before constructing the Uri fixes the parsing
side, but UriHelpers.ToQualifiedPath() used UriFormat.Unescaped when
serializing back to a string, which decoded the escape straight back
into a literal '#' -- reproducing the same truncation once the value
was written into the signed XML. Switching that one call site to
UriFormat.UriEscaped keeps '#' encoded as %23 in the manifest/
relationship XML, while ToPackagePath() (used for actual zip lookups)
is left untouched since it must keep matching raw entry names.

Fixes dotnet#998
@ChatchawanIllyes
ChatchawanIllyes requested a review from a team as a code owner August 29, 2026 18:44
…mismatch

CI caught this: Encoding.UTF8 via StreamWriter emits a BOM preamble,
but the expected digest was computed over the string's bytes without
one, so the digests never matched regardless of the production fix.
Writing the same byte array to both the zip entry and the SHA-256
hash removes the discrepancy.

The rest of the test already passed on Windows CI before this fix:
the part was found intact (no fragment truncation), signing
succeeded, and the Reference URI contained %23 with no raw '#' --
i.e. the actual fix in OpcPart.cs/UriHelpers.cs was already verified
working. Only this test's own digest comparison was wrong.
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.

Octothorpe symbol (#) in OPC part names is not handled properly

1 participant