Skip to content

Commit

Permalink
Allow CA certificates with extendedKeyUsage attributes. (#22160)
Browse files Browse the repository at this point in the history
  • Loading branch information
roperzh committed Sep 17, 2024
1 parent b3bafb8 commit a0a301b
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 5 deletions.
1 change: 1 addition & 0 deletions changes/22158-scep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Allow custom SCEP CA certificates with any kind of extendedKeyUsage attributes.
26 changes: 21 additions & 5 deletions server/mdm/crypto/scep.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"

"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/assets"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/http/mdm"
)

Expand All @@ -33,15 +34,21 @@ func (s *SCEPVerifier) Verify(cert *x509.Certificate) error {
}

// TODO(roberto): nano interfaces don't allow to pass a context to this function
assets, err := s.ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
fleet.MDMAssetCACert,
})
rootCert, err := assets.X509Cert(context.Background(), s.ds, fleet.MDMAssetCACert)
if err != nil {
return fmt.Errorf("loading existing assets from the database: %w", err)
}
opts.Roots.AddCert(rootCert)

if ok := opts.Roots.AppendCertsFromPEM(assets[fleet.MDMAssetCACert].Value); !ok {
return errors.New("unable to append cerver SCEP cert to pool verifier")
// the default SCEP cert issued by fleet doesn't have any extra key
// usages, however, customers might configure the server with any
// certificate they want (generally for touchless MDM migrations)
//
// given that go verifies ext key usages on the whole chain, we relax
// the constraints when the provided certificate has any ext key usage
// that would cause a failure.
if hasOtherKeyUsages(rootCert, x509.ExtKeyUsageClientAuth) {
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageAny}
}

if _, err := cert.Verify(opts); err != nil {
Expand All @@ -50,3 +57,12 @@ func (s *SCEPVerifier) Verify(cert *x509.Certificate) error {

return nil
}

func hasOtherKeyUsages(cert *x509.Certificate, usage x509.ExtKeyUsage) bool {
for _, u := range cert.ExtKeyUsage {
if u != usage {
return true
}
}
return false
}
139 changes: 139 additions & 0 deletions server/mdm/crypto/scep_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
package mdmcrypto

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"testing"
"time"

"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/stretchr/testify/require"
)

Expand All @@ -11,3 +23,130 @@ func TestSCEPVerifierVerifyEmptyCerts(t *testing.T) {
err := v.Verify(nil)
require.ErrorContains(t, err, "no certificate provided")
}

func TestVerify(t *testing.T) {
ds := new(mock.Store)
verifier := NewSCEPVerifier(ds)

// generate a valid root certificate with ExtKeyUsageClientAuth
validRootCertBytes, validRootCert, rootKey := generateRootCertificate(t, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
_, validClientCert := generateClientCertificate(t, validRootCert, rootKey)

// generate a root certificate with an unrelated ExtKeyUsage
rootWithOtherUsagesBytes, rootWithOtherUsageCert, rootWithOtherUsageKey := generateRootCertificate(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
_, validClientCertFromMultipleUsageRoot := generateClientCertificate(t, rootWithOtherUsageCert, rootWithOtherUsageKey)

cases := []struct {
name string
rootCert []byte
certToVerify *x509.Certificate
wantErr string
}{
{
name: "no certificate provided",
rootCert: nil,
certToVerify: nil,
wantErr: "no certificate provided",
},
{
name: "error loading root cert from database",
rootCert: nil,
certToVerify: validClientCert,
wantErr: "loading existing assets from the database",
},
{
name: "valid certificate verification succeeds",
rootCert: validRootCertBytes,
certToVerify: validClientCert,
wantErr: "",
},
{
name: "valid certificate with unrelated key usage in root cert",
rootCert: rootWithOtherUsagesBytes,
certToVerify: validClientCertFromMultipleUsageRoot,
wantErr: "",
},
{
name: "mismatched certificate presented",
rootCert: rootWithOtherUsagesBytes,
certToVerify: validClientCert,
wantErr: "certificate signed by unknown authority",
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
if tt.rootCert == nil {
return nil, errors.New("test error")
}

return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Value: tt.rootCert},
}, nil
}

err := verifier.Verify(tt.certToVerify)
if tt.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tt.wantErr)
}
})
}
}

func generateRootCertificate(t *testing.T, extKeyUsages []x509.ExtKeyUsage) ([]byte, *x509.Certificate, *ecdsa.PrivateKey) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

rootCertTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test Root CA"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
ExtKeyUsage: extKeyUsages,
BasicConstraintsValid: true,
}

rootCertDER, err := x509.CreateCertificate(rand.Reader, rootCertTemplate, rootCertTemplate, &priv.PublicKey, priv)
require.NoError(t, err)

rootCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCertDER})

rootCert, err := x509.ParseCertificate(rootCertDER)
require.NoError(t, err)

return rootCertPEM, rootCert, priv
}

func generateClientCertificate(t *testing.T, rootCert *x509.Certificate, rootKey *ecdsa.PrivateKey) ([]byte, *x509.Certificate) {
clientPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

clientCertTemplate := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{
Organization: []string{"Test Client"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(1 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}

clientCertDER, err := x509.CreateCertificate(rand.Reader, clientCertTemplate, rootCert, &clientPriv.PublicKey, rootKey)
require.NoError(t, err)

clientCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: clientCertDER})

clientCert, err := x509.ParseCertificate(clientCertDER)
require.NoError(t, err)

return clientCertPEM, clientCert
}

0 comments on commit a0a301b

Please sign in to comment.