Windows SCEP profiles now fail with non-printable chars (#49887)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #47492 

Windows cert profile fails if challenge uses non-printable characters.
<img width="987" height="329" alt="image"
src="https://github.com/user-attachments/assets/04dc7c78-8e3e-41c8-823e-cb4a961a91eb"
/>


# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **Bug Fixes**
* Windows SCEP profiles now fail with a clear error when the certificate
authority challenge includes characters not supported by Windows ASN.1
PrintableString.
* Prevents misleading “Verified” status when no certificate is
installed.
  * Preserves valid challenge values, including leading/trailing spaces.
* Improves Windows error tooltips by showing raw certificate-install
error details.

* **Tests**
* Added coverage for invalid/valid Windows SCEP challenge scenarios and
the updated error tooltip behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-07-27 12:49:14 -05:00
committed by GitHub
parent 9e680854f4
commit 19efda2d1b
5 changed files with 75 additions and 3 deletions
@@ -0,0 +1 @@
- Windows SCEP profiles now fail with a clear message when the certificate authority challenge contains characters Windows doesn't support (ASN.1 PrintableString), instead of showing "Verified" while no certificate is installed.
@@ -25,6 +25,21 @@ describe("generateErrorTooltip", () => {
expect(result).toBeNull();
});
it("renders a windows certificate install error as is, without key-value formatting", () => {
const detail = `Couldn't install certificate. The "WINSCEPTEST" certificate authority challenge includes characters Windows doesn't support. Allowed: letters, numbers, spaces, and ' ( ) + , - . / : = ?`;
const tooltip = generateErrorTooltip(
createMockHostMdmProfile({
platform: "windows",
status: "failed",
detail,
})
);
renderTooltip(tooltip);
expect(screen.getByText(detail)).toBeInTheDocument();
});
it("formats a windows profile error with key-value pairs", () => {
const tooltip = generateErrorTooltip(
createMockHostMdmProfile({
@@ -129,11 +129,12 @@ const formatDetailWindowsProfile = (detail: string) => {
const keyValuePairs = detail.split(/, */);
const formattedElements: JSX.Element[] = [];
// Special case to handle bitlocker error message. It does not follow the
// expected string format so we will just render the error message as is.
// Special case to handle bitlocker and certificate install error messages.
// They do not follow the expected string format so we will just render the error message as is.
if (
detail.includes("BitLocker") ||
detail.includes("preparing volume for encryption")
detail.includes("preparing volume for encryption") ||
detail.startsWith("Couldn't install certificate")
) {
return detail;
}
+16
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"regexp"
"slices"
"strings"
"time"
@@ -22,6 +23,16 @@ func PreprocessWindowsProfileContentsForDeployment(deps ProfilePreprocessDepende
return preprocessWindowsProfileContents(deps, params, profileContents)
}
// windowsSCEPChallengeRegexp matches challenges made up entirely of characters valid in an ASN.1 PrintableString: letters,
// digits, space, and ' ( ) + , - . / : = ?. Windows encodes the SCEP challenge password as a PrintableString, so a challenge with
// any other character (most commonly "_") makes enrollment fail on-device with "The string contains a non-printable character."
// The space is allowed anywhere, including leading and trailing, and was verified to enroll fine on Windows 11.
var windowsSCEPChallengeRegexp = regexp.MustCompile(`^[A-Za-z0-9 '()+,./:=?-]*$`)
// scepChallengeInvalidCharsDetail is the host profile failure detail shown on the Host details page when a custom SCEP proxy
// challenge contains characters Windows can't encode as a PrintableString.
const scepChallengeInvalidCharsDetail = `Couldn't install certificate. The "%s" certificate authority challenge includes characters Windows doesn't support. Allowed: letters, numbers, spaces, and ' ( ) + , - . / : = ?`
// MicrosoftProfileProcessingError is used to indicate errors during Microsoft profile processing, such as variable replacement failures.
// It should not break the entire deployment flow, but rather be handled gracefully at the profile level, setting it to failed and detail = Error()
type MicrosoftProfileProcessingError struct {
@@ -135,6 +146,11 @@ func preprocessWindowsProfileContents(deps ProfilePreprocessDependencies, params
if err != nil {
return profileContents, err
}
if ca := deps.CustomSCEPCAs[caName]; ca != nil && !windowsSCEPChallengeRegexp.MatchString(ca.Challenge) {
return profileContents, &MicrosoftProfileProcessingError{
message: fmt.Sprintf(scepChallengeInvalidCharsDetail, caName),
}
}
replacedContents, replacedVariable, err := profiles.ReplaceCustomSCEPChallengeVariable(deps.Context, deps.Logger, fleetVar, deps.CustomSCEPCAs, result)
if err != nil {
return profileContents, ctxerr.Wrap(deps.Context, err, "replacing custom SCEP challenge variable")
@@ -268,6 +268,45 @@ func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
}
},
},
{
name: "custom scep proxy challenge with character windows doesn't support",
hostUUID: "test-host-1234-uuid",
profileContents: `<Replace><Data>$FLEET_VAR_CUSTOM_SCEP_CHALLENGE_CERTIFICATE</Data></Replace>`,
expectError: true,
processingError: fmt.Sprintf(scepChallengeInvalidCharsDetail, "CERTIFICATE"),
setup: func() {
ds.GetAllCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) ([]*fleet.CertificateAuthority, error) {
return []*fleet.CertificateAuthority{
{
ID: 1,
Name: new("CERTIFICATE"),
Type: string(fleet.CATypeCustomSCEPProxy),
URL: new("https://scep.proxy.url/scep"),
Challenge: new("super_secret"),
},
}, nil
}
},
},
{
name: "custom scep proxy challenge with leading and trailing spaces preserved",
hostUUID: "test-host-1234-uuid",
profileContents: `<Replace><Data>$FLEET_VAR_CUSTOM_SCEP_CHALLENGE_CERTIFICATE</Data></Replace>`,
expectedContents: `<Replace><Data> super secret </Data></Replace>`,
setup: func() {
ds.GetAllCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) ([]*fleet.CertificateAuthority, error) {
return []*fleet.CertificateAuthority{
{
ID: 1,
Name: new("CERTIFICATE"),
Type: string(fleet.CATypeCustomSCEPProxy),
URL: new("https://scep.proxy.url/scep"),
Challenge: new(" super secret "),
},
}, nil
}
},
},
{
name: "all idp variables",
hostUUID: "idp-host-uuid",