fleet/ee/server/service/hostidentity/httpsig/middleware.go
Lucas Manuel Rodriguez 0b8c29198b
Make orbit and Fleet Desktop not depend on server/service/ packages (#42231)
Resolves #40396.

No changes file because there should be no user visible changes.

## Testing

- [x] QA'd all new/changed functionality manually

## fleetd/orbit/Fleet Desktop

- [x] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [X] Verified that fleetd runs on macOS, Linux and Windows
- [X] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))
2026-03-26 10:59:42 -03:00

121 lines
4 KiB
Go

package httpsig
import (
"context"
"fmt"
"log/slog"
"net/http"
"regexp"
"strings"
"github.com/fleetdm/fleet/v4/ee/pkg/hostidentity/types"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
)
type key int
const hostIdentityKey key = 0
var sigAuthenticatedEndpoints = []*regexp.Regexp{
regexp.MustCompile(`^/api/(?:v1|latest)/fleet/certificate_authorities/\d+/request_certificate$`),
regexp.MustCompile(`^/api/fleet/orbit/`),
regexp.MustCompile(`/osquery/`),
}
func IsSigAuthEndpoint(path string) bool {
for _, endp := range sigAuthenticatedEndpoints {
if endp.Match([]byte(path)) {
return true
}
}
return false
}
// NewContext creates a new context.Context with host identity cert.
func NewContext(ctx context.Context, hostIdentity types.HostIdentityCertificate) context.Context {
return context.WithValue(ctx, hostIdentityKey, hostIdentity)
}
// FromContext returns a pointer to the host identity cert.
func FromContext(ctx context.Context) (types.HostIdentityCertificate, bool) {
v, ok := ctx.Value(hostIdentityKey).(types.HostIdentityCertificate)
return v, ok
}
// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler.
// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed
// to it, and then calls the handler passed as parameter to the MiddlewareFunc.
type MiddlewareFunc func(http.Handler) http.Handler
func Middleware(ds fleet.Datastore, requireSignature bool, logger *slog.Logger) (MiddlewareFunc, error) {
// Initialize HTTP signature verifier
httpSig := NewHTTPSig(ds, logger)
verifier, err := httpSig.Verifier()
if err != nil {
return nil, fmt.Errorf("setup httpsig verifier: %w", err)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !IsSigAuthEndpoint(req.URL.Path) {
next.ServeHTTP(w, req)
return
}
// We do not verify the "ping" endpoint since it is used to get server capabilities and does not carry any data.
// This endpoint is unauthenticated.
if strings.HasSuffix(req.URL.Path, "/api/fleet/orbit/ping") {
next.ServeHTTP(w, req)
return
}
// If the request does not have an HTTP message signature, we do not verify it AND
// we do not set the host identity cert in the context
if req.Header.Get("signature") == "" || req.Header.Get("signature-input") == "" {
if requireSignature {
handleError(req.Context(), w,
ctxerr.Errorf(req.Context(), "missing required HTTP message signature: path=%s", req.URL.Path),
http.StatusUnauthorized)
return
}
next.ServeHTTP(w, req)
return
}
// Verify signature using certificate associated with the provided serial number.
result, err := verifier.Verify(req)
if err != nil {
handleError(req.Context(), w,
ctxerr.Wrap(req.Context(), err, "failed to verify request signature", fmt.Sprintf("path=%s", req.URL.Path)),
http.StatusUnauthorized)
return
}
keySpecer, ok := result.KeySpecer.(*KeySpecer)
if !ok {
handleError(req.Context(), w,
ctxerr.New(req.Context(), fmt.Sprintf("could not extract host identity certificate key: path=%s", req.URL.Path)),
http.StatusInternalServerError)
return
}
if !result.Verified {
handleError(req.Context(), w,
ctxerr.New(req.Context(), fmt.Sprintf("request not verified: path=%s host_uuid=%s", req.URL.Path,
keySpecer.hostIdentityCert.CommonName)),
http.StatusUnauthorized)
return
}
logger.DebugContext(req.Context(), "httpsig verified", "host_id", keySpecer.hostIdentityCert.HostID)
// Signature is valid, we set the identity data in the context and proceed with processing the request.
req = req.WithContext(NewContext(req.Context(), keySpecer.hostIdentityCert))
next.ServeHTTP(w, req)
})
}, nil
}
func handleError(ctx context.Context, w http.ResponseWriter, err error, code int) {
ctxerr.Handle(ctx, err)
http.Error(w, err.Error(), code)
}