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:
@@ -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.
|
||||
+15
@@ -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({
|
||||
|
||||
+4
-3
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user