Added DigiCert UPN support. (#27006)

For #26609

- Added User Principal Name (UPN) support for DigiCert integration. We
currently support only 1 UPN.

# Checklist for submitter

- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Victor Lyuboslavsky 2025-03-10 16:02:23 -05:00 committed by GitHub
parent 6bbf645c10
commit 57ef7b36ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 64 additions and 17 deletions

View file

@ -106,8 +106,6 @@ func GetCertificate(ctx context.Context, logger kitlog.Logger, config fleet.Digi
},
}
// TODO(#26609): Add support for User Principal Name
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, privateKey)
if err != nil {
return nil, "", ctxerr.Wrap(ctx, err, "creating CSR")
@ -133,6 +131,19 @@ func GetCertificate(ctx context.Context, logger kitlog.Logger, config fleet.Digi
},
"csr": csr,
}
// UPN (User Principal Names) is only supported by User seat type (2025/03/10)
// https://docs.digicert.com/fr/trust-lifecycle-manager/inventory/certificate-attributes-and-extensions/subject-alternative-name--san--attributes.html
if len(config.CertificateUserPrincipalNames) > 0 {
attributes, ok := reqBody["attributes"].(map[string]interface{})
if !ok {
return nil, "", ctxerr.Errorf(ctx, "unexpected DigiCert attributes type: %T", reqBody["attributes"])
}
attributes["extensions"] = map[string]interface{}{
"san": map[string]interface{}{
"user_principal_names": config.CertificateUserPrincipalNames,
},
}
}
bodyBytes, err := json.Marshal(reqBody)
if err != nil {

View file

@ -13245,7 +13245,7 @@ func (s *integrationMDMTestSuite) TestSCEPProxy() {
func (s *integrationMDMTestSuite) TestDigiCertConfig() {
t := s.T()
ctx := context.Background()
mockDigiCertServer := createMockDigiCertServer(t)
digiCertServer := createMockDigiCertServer(t)
// Add DigiCert integration with bad URL
caBad := getDigiCertIntegration("https://httpstat.us/410", "ca")
@ -13265,11 +13265,11 @@ func (s *integrationMDMTestSuite) TestDigiCertConfig() {
assert.True(t, fleet.IsNotFound(err))
// Add 3 DigiCert integrations
ca0 := getDigiCertIntegration(mockDigiCertServer.URL, "ca0")
ca0 := getDigiCertIntegration(digiCertServer.server.URL, "ca0")
ca0.APIToken = "api_token0"
ca1 := getDigiCertIntegration(mockDigiCertServer.URL, "ca1")
ca1 := getDigiCertIntegration(digiCertServer.server.URL, "ca1")
ca1.APIToken = "api_token1"
ca2 := getDigiCertIntegration(mockDigiCertServer.URL, "ca2")
ca2 := getDigiCertIntegration(digiCertServer.server.URL, "ca2")
ca2.APIToken = "api_token2"
appConfig = map[string]interface{}{
"integrations": map[string]interface{}{
@ -13321,7 +13321,7 @@ func (s *integrationMDMTestSuite) TestDigiCertConfig() {
// Add 1, modify 1, delete 1, keep 1 the same (DigiCert integrations)
ca1.URL += "//"
ca3 := getDigiCertIntegration(mockDigiCertServer.URL, "ca3")
ca3 := getDigiCertIntegration(digiCertServer.server.URL, "ca3")
ca3.APIToken = "api_token3"
appConfig = map[string]interface{}{
"integrations": map[string]interface{}{
@ -13427,10 +13427,10 @@ func (s *integrationMDMTestSuite) TestDigiCertConfig() {
func (s *integrationMDMTestSuite) TestDigiCertIntegration() {
t := s.T()
ctx := context.Background()
mockDigiCertServer := createMockDigiCertServer(t)
digiCertServer := createMockDigiCertServer(t)
// Add DigiCert config
ca := getDigiCertIntegration(mockDigiCertServer.URL, "my_CA")
ca := getDigiCertIntegration(digiCertServer.server.URL, "my_CA")
ca.APIToken = "api_token0"
appConfig := map[string]interface{}{
"integrations": map[string]interface{}{
@ -13515,8 +13515,26 @@ func (s *integrationMDMTestSuite) TestDigiCertIntegration() {
require.NoError(t, err)
assert.Equal(t, ca.CertificateCommonName, certificate.Subject.CommonName)
// Check request to DigiCert
digiCertServer.certReqMu.Lock()
assert.Equal(t, ca.ProfileID, digiCertServer.certReq.Profile["id"])
assert.Equal(t, ca.CertificateSeatID, digiCertServer.certReq.Seat["seat_id"])
assert.Equal(t, "x509", digiCertServer.certReq.DeliveryFormat)
assert.Equal(t, ca.CertificateCommonName, digiCertServer.certReq.Attributes["subject"].(map[string]interface{})["common_name"])
// Need to convert UPNs to string slice since assert.Equal cannot compare string slice and interface slice
upnsReceived := digiCertServer.certReq.Attributes["extensions"].(map[string]interface{})["san"].(map[string]interface{})["user_principal_names"].([]interface{})
var stringSlice []string
for _, item := range upnsReceived {
if str, ok := item.(string); ok {
stringSlice = append(stringSlice, str)
}
}
assert.Equal(t, ca.CertificateUserPrincipalNames, stringSlice)
digiCertServer.certReqMu.Unlock()
// Try a DigiCert CA that will fail
caFail := getDigiCertIntegration(mockDigiCertServer.URL, "fail_CA")
caFail := getDigiCertIntegration(digiCertServer.server.URL, "fail_CA")
caFail.CertificateCommonName = "Fail"
appConfig = map[string]interface{}{
"integrations": map[string]interface{}{
@ -13606,10 +13624,27 @@ func digiCertForTest(name, identifier, caName string) []byte {
`, caName, caName, name, identifier, uuid.New().String()))
}
func createMockDigiCertServer(t *testing.T) *httptest.Server {
type mockDigiCertServer struct {
server *httptest.Server
certReq certificateReq
certReqMu *sync.Mutex
}
type certificateReq struct {
CSR string `json:"csr"`
Profile map[string]string `json:"profile"`
Seat map[string]string `json:"seat"`
DeliveryFormat string `json:"delivery_format"`
Attributes map[string]interface{} `json:"attributes"`
}
func createMockDigiCertServer(t *testing.T) *mockDigiCertServer {
profileRegex := regexp.MustCompile(`^/mpki/api/v2/profile/([a-zA-Z0-9_-]+)$`)
certRegex := regexp.MustCompile(`^/mpki/api/v1/certificate`)
mockDigiCertServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mockServer := &mockDigiCertServer{
certReqMu: &sync.Mutex{},
}
mockServer.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
@ -13632,11 +13667,12 @@ func createMockDigiCertServer(t *testing.T) *httptest.Server {
w.WriteHeader(http.StatusBadRequest)
return
}
var req struct {
CSR string `json:"csr"`
}
mockServer.certReqMu.Lock()
defer mockServer.certReqMu.Unlock()
var req certificateReq
err := json.NewDecoder(r.Body).Decode(&req)
require.NoError(t, err)
mockServer.certReq = req
// Decode the PEM format CSR into DER
block, _ := pem.Decode([]byte(req.CSR))
@ -13702,8 +13738,8 @@ func createMockDigiCertServer(t *testing.T) *httptest.Server {
w.WriteHeader(http.StatusMethodNotAllowed)
}
}))
t.Cleanup(mockDigiCertServer.Close)
return mockDigiCertServer
t.Cleanup(mockServer.server.Close)
return mockServer
}
type noopCertDepot struct{ depot.Depot }