💥 Actual behavior
Fleet's SCEP proxy returns HTTP 400 "failed to base64 decode message: illegal base64 data at input byte N" for any GET PKIOperation whose base64 message query parameter contains a literal +. Because base64 produces + for roughly 1 in 16 input bytes, virtually every real PKCS#7 envelope triggers it.
Root cause: server/mdm/scep/server/transport.go's message() reads the SCEP message via r.URL.Query().Get("message"). url.Values.Get() performs HTML-form decoding, which converts a literal + into a space. That is correct for application/x-www-form-urlencoded bodies but wrong for RFC 3986 query strings, where + is a literal +. The space breaks base64 decoding at the first replaced byte.
Scope: any SCEP client doing GET PKIOperation without percent-escaping + — Fleet Android agent (jScep), some Microsoft NDES configurations that fall back to GET, embedded SCEP clients. POST PKIOperation puts the payload in the request body and is unaffected. Clients negotiate GET vs POST from the CA's GetCACaps response, so a CA that advertises POSTPKIOperation masks the bug; any CA misconfiguration or older client that uses GET re-exposes it.
🛠️ Expected behavior
We could simply document that we do not support GET PKIOperation and use POST instead (which almost everyone should already be using). But it is possible for someone to point Fleet at an old/misconfigured CA that is not configured to use POST.
The SCEP proxy should accept GET PKIOperation whether or not the client percent-escapes + and /. In transport.go's message(), parse the message value from r.URL.RawQuery directly so literal + is preserved, then url.PathUnescape and base64.StdEncoding.DecodeString as before. This is harmless for clients that do percent-escape — %2B round-trips through RawQuery plus PathUnescape exactly as before.
This is the opposite-side companion to micromdm/scep PR #250, which fixed the client encoder to send + (escaped to %2B) instead of -/_. This issue is about servers tolerating + whether or not the client escaped it.
🧑💻 Steps to reproduce
These steps:
Device-free repro with curl:
-
Configure a custom SCEP CA on a Fleet team and create a certificate template; note its template ID and a host's host_uuid on that team.
-
Grab any base64 PKCS#7 PKIOperation payload that contains at least one +.
-
Hit the proxy with the literal-+ form (do not percent-escape):
MSG="$(base64 < pkcs7.bin | tr -d '\n')"
curl -vk "https://<fleet>/mdm/scep/proxy/<host_uuid>,g<template_id>,custom_scep_proxy,<challenge>?operation=PKIOperation&message=${MSG}"
-
Observe HTTP 400 "failed to base64 decode message: illegal base64 data at input byte N: ..." where N is the offset of the first +.
-
Repeat with + escaped to %2B and / to %2F: the request succeeds (modulo upstream CA). This confirms the failure is server-side decoding of literal +, not the payload.
🕯️ More info (optional)
💥 Actual behavior
Fleet's SCEP proxy returns HTTP 400
"failed to base64 decode message: illegal base64 data at input byte N"for any GETPKIOperationwhose base64messagequery parameter contains a literal+. Because base64 produces+for roughly 1 in 16 input bytes, virtually every real PKCS#7 envelope triggers it.Root cause:
server/mdm/scep/server/transport.go'smessage()reads the SCEP message viar.URL.Query().Get("message").url.Values.Get()performs HTML-form decoding, which converts a literal+into a space. That is correct forapplication/x-www-form-urlencodedbodies but wrong for RFC 3986 query strings, where+is a literal+. The space breaks base64 decoding at the first replaced byte.Scope: any SCEP client doing GET
PKIOperationwithout percent-escaping+— Fleet Android agent (jScep), some Microsoft NDES configurations that fall back to GET, embedded SCEP clients. POSTPKIOperationputs the payload in the request body and is unaffected. Clients negotiate GET vs POST from the CA'sGetCACapsresponse, so a CA that advertisesPOSTPKIOperationmasks the bug; any CA misconfiguration or older client that uses GET re-exposes it.🛠️ Expected behavior
We could simply document that we do not support GET
PKIOperationand use POST instead (which almost everyone should already be using). But it is possible for someone to point Fleet at an old/misconfigured CA that is not configured to use POST.The SCEP proxy should accept GET
PKIOperationwhether or not the client percent-escapes+and/. Intransport.go'smessage(), parse themessagevalue fromr.URL.RawQuerydirectly so literal+is preserved, thenurl.PathUnescapeandbase64.StdEncoding.DecodeStringas before. This is harmless for clients that do percent-escape —%2Bround-trips throughRawQueryplusPathUnescapeexactly as before.This is the opposite-side companion to micromdm/scep PR #250, which fixed the client encoder to send
+(escaped to%2B) instead of-/_. This issue is about servers tolerating+whether or not the client escaped it.🧑💻 Steps to reproduce
These steps:
Device-free repro with
curl:Configure a custom SCEP CA on a Fleet team and create a certificate template; note its template ID and a host's
host_uuidon that team.Grab any base64 PKCS#7
PKIOperationpayload that contains at least one+.Hit the proxy with the literal-
+form (do not percent-escape):Observe HTTP 400
"failed to base64 decode message: illegal base64 data at input byte N: ..."where N is the offset of the first+.Repeat with
+escaped to%2Band/to%2F: the request succeeds (modulo upstream CA). This confirms the failure is server-side decoding of literal+, not the payload.🕯️ More info (optional)