Skip to content

Fix REG_DWORD handling in ConfigService.GetRegistryValue#6

Merged
rodchristiansen merged 2 commits into
mainfrom
fix/registry-dword-handling
Apr 11, 2026
Merged

Fix REG_DWORD handling in ConfigService.GetRegistryValue#6
rodchristiansen merged 2 commits into
mainfrom
fix/registry-dword-handling

Conversation

@rodchristiansen

Copy link
Copy Markdown
Contributor

Closes #4.

Summary

`GetRegistryValue` was reading raw values with `key?.GetValue(name) as string`, which returns null for any non-string registry type. `REG_DWORD` values come back from `RegistryKey.GetValue` as boxed `int`, so every CSP-deployed boolean or integer policy that used the standard DWORD wire format was silently dropped.

Affected helpers (all documented as `String/DWORD` in the README, but only actually working for string-typed deployments):

  • `GetUseMtls`
  • `GetAutoRotate`
  • `GetSkipCertCheck`
  • `GetCleanupOldProtectors`
  • `GetKeyEscrowIntervalHours`
  • `GetValidateKey`

The "Handle registry DWORD values" fall-through at `ConfigService.cs:115` in `GetRegistryBool` was unreachable.

Fix

Factor out `ConvertToConfigString` as an internal pure helper and route both production paths (`SOFTWARE\Policies...` and `SOFTWARE\Microsoft\PolicyManager...`) through it:

Raw type Conversion
`string` (REG_SZ, REG_EXPAND_SZ) Returned as-is; empty/whitespace → null
`int` (REG_DWORD) Invariant-culture `int.ToString`
`long` (REG_QWORD) Invariant-culture `long.ToString`
Anything else (REG_BINARY, REG_MULTI_SZ, …) null → caller falls through

`GetRegistryBool` and `GetRegistryInt` now receive the string form of DWORDs and their existing `int.TryParse` branches light up correctly.

Tests (+22, 61 total, ~1s runtime)

New `RegistryValueConversionTests` class (joins `GlobalStateCollection` since it mutates `ConfigService` static state):

  • 8 pure unit tests on `ConvertToConfigString` — null, empty/whitespace strings, positive/negative/zero/MaxValue/MinValue DWORDs, QWORD max, byte arrays and string arrays.
  • 3 integration tests that open a real HKCU subkey via `TempRegistryKey.OpenKey()`, call `GetValue` on `REG_SZ` / `REG_DWORD` / `REG_QWORD` entries, and assert `ConvertToConfigString` returns the expected string. These prove the boxed-int/long round trip works against actual registry data — the part of the production chain that the old `as string` cast dropped.
  • 4 end-to-end tests (`GetSkipCertCheck`, `GetKeyEscrowIntervalHours`, `GetAutoRotate`) exercising the full `GetRegistryBool`/`Int` path with DWORD values.

Test seam changes

  • `TempRegistryKey.ReadValue` now delegates to `ConfigService.ConvertToConfigString` instead of having its own private switch, so production and test conversion logic can never drift apart.
  • New `TempRegistryKey.OpenKey()` returns the underlying HKCU key so tests can exercise the raw `GetValue` chain directly.
  • New `TempRegistryKey.SetQword()` for QWORD coverage.

Test plan

  • `dotnet build CryptEscrow.sln` — 0 warnings, 0 errors
  • `dotnet test CryptEscrow.sln -c Release` — 61 passed, 0 failed in ~1s
  • CI run on this PR passes
  • Manual verification: deploy `UseMtls` as `REG_DWORD = 1` via Intune CSP on a test box and confirm `GetUseMtls()` returns `true` after this change

Copilot AI review requested due to automatic review settings April 11, 2026 23:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes ConfigService.GetRegistryValue so enterprise policy registry values stored as REG_DWORD/REG_QWORD are no longer silently ignored, enabling CSP-deployed boolean/integer policies to work as documented.

Changes:

  • Refactors GetRegistryValue to read raw registry values and convert supported types via a new ConvertToConfigString helper.
  • Updates the TempRegistryKey test fixture to reuse production conversion logic and adds helpers to support QWORD + raw RegistryKey.GetValue() testing.
  • Adds RegistryValueConversionTests with unit + integration + end-to-end coverage for DWORD/QWORD conversions and affected getters.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/CryptEscrow/Services/ConfigService.cs Adds ConvertToConfigString and routes both registry paths through shared raw-value conversion logic.
tests/CryptEscrow.Tests/Fixtures/TempRegistryKey.cs Aligns fixture registry reads with production conversion and adds QWORD + direct key access for integration tests.
tests/CryptEscrow.Tests/Services/RegistryValueConversionTests.cs Adds regression coverage for conversion and end-to-end DWORD policy parsing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/CryptEscrow/Services/ConfigService.cs Outdated
Comment thread src/CryptEscrow/Services/ConfigService.cs Outdated
rodchristiansen added a commit that referenced this pull request Apr 11, 2026
1. Rename Serilog property placeholder {Path} to {PolicySource} in
   ReadValueFromPath. Elsewhere in this codebase {Path} is used for
   actual filesystem/registry paths (e.g. 'mTLS enabled via PEM
   files: {Path}' in CryptServerClient.cs); reusing the same name
   with 'GP'/'MDM' labels would make structured log queries
   misleading.

2. Reword GetRegistryValue XML doc summary. Previously said 'Reads a
   string value' which was accurate before the DWORD/QWORD fix but is
   now misleading — the method accepts multiple registry types and
   returns their string representation. Updated to 'Reads an
   enterprise policy value from the registry ... and returns its
   string representation.'

No behavior change; docs + log template only. 61 tests still passing.
Closes #4.

GetRegistryValue was reading raw values with `key?.GetValue(name) as string`,
which returns null for any non-string registry type. REG_DWORD values come
back from RegistryKey.GetValue as boxed int — so every CSP-deployed
boolean or integer policy that used the standard DWORD wire format was
silently dropped. This affected GetUseMtls, GetAutoRotate,
GetSkipCertCheck, GetCleanupOldProtectors, GetKeyEscrowIntervalHours, and
GetValidateKey — all documented as String/DWORD in the README but only
actually working for strings.

The "Handle registry DWORD values" fall-through in GetRegistryBool
(ConfigService.cs:115) was unreachable.

## Fix

Factor out ConvertToConfigString as an internal pure helper that
converts a raw registry value to its string form, and route both
production paths (GP and MDM) through it:

- string / REG_SZ / REG_EXPAND_SZ → returned as-is (null for empty)
- int / REG_DWORD → invariant-culture int.ToString
- long / REG_QWORD → invariant-culture long.ToString
- anything else (REG_BINARY, REG_MULTI_SZ, …) → null (caller falls through)

GetRegistryBool and GetRegistryInt now receive the string form of DWORDs
and the existing int.TryParse branches light up.

## Tests (22 new, 61 total passing in ~1s)

RegistryValueConversionTests:
- 8 pure unit tests on ConvertToConfigString covering null, empty/whitespace
  strings, positive/negative/zero/MaxValue/MinValue DWORDs, QWORD max,
  byte arrays and string arrays (both null).
- 3 integration tests that open a real HKCU subkey via TempRegistryKey,
  call GetValue on REG_SZ/REG_DWORD/REG_QWORD entries, and assert
  ConvertToConfigString returns the expected string. These prove the
  boxed int/long round trip works against actual registry data — the
  part of the production chain that the old `as string` cast dropped.
- 4 end-to-end tests (GetSkipCertCheck, GetKeyEscrowIntervalHours,
  GetAutoRotate) exercising the full GetRegistryBool/Int path with
  DWORD values.

## Test seam changes

TempRegistryKey.ReadValue now delegates to ConfigService.ConvertToConfigString
instead of having its own private switch, so production and test conversion
logic can never drift apart. New TempRegistryKey.OpenKey() returns the
underlying HKCU key so tests can exercise the raw GetValue chain directly.
TempRegistryKey.SetQword added for QWORD coverage.

## Joins GlobalState collection

RegistryValueConversionTests uses TempRegistryKey and TempConfigFile (both
mutate ConfigService static state), so the class joins the existing
GlobalStateCollection to opt out of cross-class parallelism.
1. Rename Serilog property placeholder {Path} to {PolicySource} in
   ReadValueFromPath. Elsewhere in this codebase {Path} is used for
   actual filesystem/registry paths (e.g. 'mTLS enabled via PEM
   files: {Path}' in CryptServerClient.cs); reusing the same name
   with 'GP'/'MDM' labels would make structured log queries
   misleading.

2. Reword GetRegistryValue XML doc summary. Previously said 'Reads a
   string value' which was accurate before the DWORD/QWORD fix but is
   now misleading — the method accepts multiple registry types and
   returns their string representation. Updated to 'Reads an
   enterprise policy value from the registry ... and returns its
   string representation.'

No behavior change; docs + log template only. 61 tests still passing.
@rodchristiansen rodchristiansen force-pushed the fix/registry-dword-handling branch from 185f971 to d5b1f0c Compare April 11, 2026 23:43
@rodchristiansen rodchristiansen merged commit 7a6b1b3 into main Apr 11, 2026
2 checks passed
@rodchristiansen rodchristiansen deleted the fix/registry-dword-handling branch April 11, 2026 23:49
rodchristiansen added a commit that referenced this pull request Apr 12, 2026
Consolidates the four PRs merged today into a single semver entry
for humans, and triggers a new CI release whose body will link back
to the individual timestamp-tagged releases (2308, 2314, 2344, 2347).

The entry credits @aysiu explicitly for the PEM client cert
contribution in PR #2, which was the original spark for the mTLS
feature. PFX + Credential Manager and the strategy-priority refactor
were layered on as maintainer tweaks.

Also documents:
- xUnit test suite + dotnet test CI integration (PR #3)
- Node.js 24 opt-in for JavaScript Actions (PR #5)
- REG_DWORD/QWORD handling fix in GetRegistryValue, closing #4 (PR #6)
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.

ConfigService.GetRegistryValue silently drops REG_DWORD policy values

2 participants