mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Fixes #30458 Contributor docs PR: https://github.com/fleetdm/fleet/pull/30651 # Checklist for submitter - We will add changes file later. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Added/updated automated tests - Did not do manual QA since the SCEP client I have doesn't support ECC. Will rely on next subtasks for manual QA. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced Host Identity SCEP (Simple Certificate Enrollment Protocol) support, enabling secure host identity certificate enrollment and management. * Added new API endpoints for Host Identity SCEP, including certificate issuance and retrieval. * Implemented MySQL-backed storage and management for host identity SCEP certificates and serials. * Added new database tables for storing host identity SCEP certificates and serial numbers. * Provided utilities for encoding certificates and keys, and handling ECDSA public keys. * **Bug Fixes** * None. * **Tests** * Added comprehensive integration and unit tests for Host Identity SCEP functionality, including certificate issuance, validation, and error scenarios. * **Chores** * Updated test utilities to support unique test names and new SCEP storage options. * Extended mock datastore and interfaces for new host identity certificate methods. * **Documentation** * Added comments and documentation for new SCEP-related interfaces, methods, and database schema changes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
221 lines
5.5 KiB
Go
221 lines
5.5 KiB
Go
package apple_mdm
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
|
|
)
|
|
|
|
const (
|
|
defaultFleetDMAPIURL = "https://fleetdm.com"
|
|
getSignedAPNSCSRPath = "/api/v1/deliver-apple-csr"
|
|
depCertificateCommonName = "Fleet"
|
|
depCertificateExpiryDays = 30
|
|
)
|
|
|
|
// emailAddressOID defined by https://oidref.com/1.2.840.113549.1.9.1
|
|
var emailAddressOID = []int{1, 2, 840, 113549, 1, 9, 1}
|
|
|
|
// GenerateAPNSCSRKey generates a APNS CSR (certificate signing request) and
|
|
// returns the CSR and private key.
|
|
func GenerateAPNSCSRKey(email, org string) (*x509.CertificateRequest, *rsa.PrivateKey, error) {
|
|
key, err := newPrivateKey()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("generate private key: %w", err)
|
|
}
|
|
|
|
subj := pkix.Name{
|
|
Organization: []string{org},
|
|
ExtraNames: []pkix.AttributeTypeAndValue{{
|
|
Type: emailAddressOID,
|
|
Value: email,
|
|
}},
|
|
}
|
|
template := &x509.CertificateRequest{
|
|
Subject: subj,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
}
|
|
|
|
b, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
certReq, err := x509.ParseCertificateRequest(b)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return certReq, key, nil
|
|
}
|
|
|
|
func GenerateAPNSCSR(org, email string, key crypto.PrivateKey) (*x509.CertificateRequest, error) {
|
|
subj := pkix.Name{
|
|
Organization: []string{org},
|
|
ExtraNames: []pkix.AttributeTypeAndValue{{
|
|
Type: emailAddressOID,
|
|
Value: email,
|
|
}},
|
|
}
|
|
template := &x509.CertificateRequest{
|
|
Subject: subj,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
}
|
|
|
|
b, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certReq, err := x509.ParseCertificateRequest(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return certReq, nil
|
|
}
|
|
|
|
func NewPrivateKey() (*rsa.PrivateKey, error) {
|
|
return newPrivateKey()
|
|
}
|
|
|
|
type FleetWebsiteError struct {
|
|
Status int
|
|
message string
|
|
}
|
|
|
|
func (e FleetWebsiteError) Error() string {
|
|
if e.message != "" {
|
|
return e.message
|
|
}
|
|
|
|
return "Unknown Error"
|
|
}
|
|
|
|
type getSignedAPNSCSRRequest struct {
|
|
UnsignedCSRData []byte `json:"unsignedCsrData"`
|
|
}
|
|
|
|
// GetSignedAPNSCSR makes a request to the fleetdm.com API to get a signed APNs
|
|
// CSR that is sent to the email provided in the certificate subject.
|
|
func GetSignedAPNSCSR(client *http.Client, csr *x509.CertificateRequest) error {
|
|
csrPEM := EncodeCertRequestPEM(csr)
|
|
|
|
payload := getSignedAPNSCSRRequest{
|
|
UnsignedCSRData: csrPEM,
|
|
}
|
|
|
|
b, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal payload: %w", err)
|
|
}
|
|
|
|
// for testing
|
|
baseURL := defaultFleetDMAPIURL
|
|
if x := os.Getenv("TEST_FLEETDM_API_URL"); x != "" {
|
|
baseURL = strings.TrimRight(x, "/")
|
|
}
|
|
u := baseURL + getSignedAPNSCSRPath
|
|
|
|
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
return FleetWebsiteError{Status: resp.StatusCode, message: string(b)}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type websiteSignCSRResponse struct {
|
|
CSR []byte `json:"csr"`
|
|
}
|
|
|
|
// GetSignedAPNSCSRNoEmail makes a request to the fleetdm.com API to get a signed APNs
|
|
// CSR and returns the signed CSR directly.
|
|
func GetSignedAPNSCSRNoEmail(client *http.Client, csr *x509.CertificateRequest) ([]byte, error) {
|
|
csrPEM := EncodeCertRequestPEM(csr)
|
|
|
|
payload := getSignedAPNSCSRRequest{
|
|
UnsignedCSRData: csrPEM,
|
|
}
|
|
|
|
b, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal payload: %w", err)
|
|
}
|
|
|
|
// for testing
|
|
baseURL := defaultFleetDMAPIURL
|
|
if x := os.Getenv("TEST_FLEETDM_API_URL"); x != "" {
|
|
baseURL = strings.TrimRight(x, "/")
|
|
}
|
|
u := baseURL + getSignedAPNSCSRPath + "?deliveryMethod=json"
|
|
|
|
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating csr signing request for fleetdm api: %w", err)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sending csr signing request to fleetdm api: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing CSR body response from fleetdm api: %w", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, FleetWebsiteError{Status: resp.StatusCode, message: string(respBytes)}
|
|
}
|
|
|
|
var csrResp websiteSignCSRResponse
|
|
if err := json.Unmarshal(respBytes, &csrResp); err != nil {
|
|
return nil, fmt.Errorf("unmarshalling signed csr response from fleetdm api: %w", err)
|
|
}
|
|
|
|
return csrResp.CSR, nil
|
|
}
|
|
|
|
// NewSCEPCACertKey creates a self-signed CA certificate for use with SCEP and
|
|
// returns the certificate and its private key.
|
|
func NewSCEPCACertKey() (*x509.Certificate, *rsa.PrivateKey, error) {
|
|
return depot.NewSCEPCACertKey()
|
|
}
|
|
|
|
// NEWDEPKeyPairPEM generates a new public key certificate and private key for downloading the Apple DEP token.
|
|
// The public key is returned as a PEM encoded certificate.
|
|
func NewDEPKeyPairPEM() ([]byte, []byte, error) {
|
|
// Note, Apple doesn't check the expiry
|
|
key, cert, err := tokenpki.SelfSignedRSAKeypair(depCertificateCommonName, depCertificateExpiryDays)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("generate encryption keypair: %w", err)
|
|
}
|
|
|
|
publicKeyPEM := tokenpki.PEMCertificate(cert.Raw)
|
|
privateKeyPEM := tokenpki.PEMRSAPrivateKey(key)
|
|
|
|
return publicKeyPEM, privateKeyPEM, nil
|
|
}
|