Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/52601-windows-mdm-client-cert-validity
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed Windows MDM client certificate backdating where NotBefore was backdated by the 180-day renewal period, properly setting validity to 365 days from issuance with a 10-minute clock-skew allowance and aligning WstepCertRenewalPeriodInDays to 180 days.
4 changes: 2 additions & 2 deletions server/mdm/microsoft/syncml/syncml.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ const (
// Supported Enroll Type Full
ReqSecTokenEnrollTypeFull = "Full"

// Provisioning Doc Certificate Renewal Period (365 days)
WstepCertRenewalPeriodInDays = "365"
// Provisioning Doc Certificate Renewal Period in Days (180 days, matching PolicyCertRenewalPeriodInSecs)
WstepCertRenewalPeriodInDays = "180"

// Provisioning Doc Server supports ROBO auto certificate renewal
// TODO: Add renewal support
Expand Down
16 changes: 10 additions & 6 deletions server/mdm/microsoft/wstep.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,18 @@ func azureDataFromClaims(ctx context.Context, claims jwt.MapClaims) (AzureData,
}, nil
}

// populateClientCert constructs an x509 client certificate template for Windows MDM enrollment,
// configuring the certificate validity period derived from MDM policy settings.
func populateClientCert(sn *big.Int, subject string, issuerCert *x509.Certificate, csr *x509.CertificateRequest) (*x509.Certificate, error) {
certRenewalPeriodInSecsInt, err := strconv.Atoi(syncml.PolicyCertRenewalPeriodInSecs)
certValidityPeriodInSecsInt, err := strconv.Atoi(syncml.PolicyCertValidityPeriodInSecs)
if err != nil {
return nil, fmt.Errorf("invalid renewal time: %w", err)
return nil, fmt.Errorf("invalid validity time: %w", err)
}

notBeforeDuration := time.Now().Add(time.Duration(certRenewalPeriodInSecsInt) * -time.Second)
yearDuration := 365 * 24 * time.Hour
// Minor clock-skew allowance of 10 minutes to accommodate client clock variations
now := time.Now()
notBefore := now.Add(-10 * time.Minute)
notAfter := now.Add(time.Duration(certValidityPeriodInSecsInt) * time.Second)

certSubject := pkix.Name{
OrganizationalUnit: []string{syncml.DocProvisioningAppProviderID},
Expand All @@ -508,8 +512,8 @@ func populateClientCert(sn *big.Int, subject string, issuerCert *x509.Certificat
EmailAddresses: csr.EmailAddresses,
DNSNames: csr.DNSNames,
URIs: csr.URIs,
NotBefore: notBeforeDuration,
NotAfter: notBeforeDuration.Add(yearDuration),
NotBefore: notBefore,
NotAfter: notAfter,
SerialNumber: sn,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,

Expand Down
46 changes: 46 additions & 0 deletions server/mdm/microsoft/wstep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"testing"
"time"

"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/cryptoutil"
"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -371,3 +374,46 @@ func TestAzureDataFromClaims(t *testing.T) {
require.Equal(t, "user@example.com", data.UPN)
})
}

func TestPopulateClientCertValidityAndNotBefore(t *testing.T) {
issuerCert, err := cryptoutil.DecodePEMCertificate(testCert)
require.NoError(t, err)

sn := big.NewInt(12345)
subject := "test-device-uuid"
csr := &x509.CertificateRequest{
Version: 1,
}

beforeCall := time.Now()
cert, err := populateClientCert(sn, subject, issuerCert, csr)
afterCall := time.Now()
require.NoError(t, err)
require.NotNil(t, cert)

// NotBefore should be within 10m clock-skew allowance, not backdated by 180 days (#52601)
expectedNotBeforeMin := beforeCall.Add(-10*time.Minute - 5*time.Second)
expectedNotBeforeMax := afterCall.Add(-10*time.Minute + 5*time.Second)
require.True(t, cert.NotBefore.After(expectedNotBeforeMin) && cert.NotBefore.Before(expectedNotBeforeMax),
"NotBefore should be ~10m in the past (clock-skew), got %v", cert.NotBefore)

// NotAfter should be derived from syncml.PolicyCertValidityPeriodInSecs
validitySecs, err := strconv.ParseInt(syncml.PolicyCertValidityPeriodInSecs, 10, 64)
require.NoError(t, err)
expectedValidity := time.Duration(validitySecs) * time.Second

expectedNotAfterMin := beforeCall.Add(expectedValidity - 5*time.Second)
expectedNotAfterMax := afterCall.Add(expectedValidity + 5*time.Second)
require.True(t, cert.NotAfter.After(expectedNotAfterMin) && cert.NotAfter.Before(expectedNotAfterMax),
"NotAfter should match PolicyCertValidityPeriodInSecs from now, got %v", cert.NotAfter)

// The effective remaining validity from issuance time must closely track PolicyCertValidityPeriodInSecs
validityFromNow := time.Until(cert.NotAfter)
require.Greater(t, validityFromNow, expectedValidity-24*time.Hour, "validity must match policy period, not halved by renewal backdating")

// Verify WstepCertRenewalPeriodInDays matches PolicyCertRenewalPeriodInSecs converted to days
renewalSecs, err := strconv.ParseInt(syncml.PolicyCertRenewalPeriodInSecs, 10, 64)
require.NoError(t, err)
expectedRenewalDays := fmt.Sprintf("%d", renewalSecs/(24*60*60))
require.Equal(t, expectedRenewalDays, syncml.WstepCertRenewalPeriodInDays)
}
Loading