mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 08:28:52 +00:00
Allow CA certificates with extendedKeyUsage attributes. (#22160)
This commit is contained in:
parent
f135d5b732
commit
2bfbf2fe3f
3 changed files with 161 additions and 5 deletions
1
changes/22158-scep
Normal file
1
changes/22158-scep
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Allow custom SCEP CA certificates with any kind of extendedKeyUsage attributes.
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue