fleet/server/mdm/apple/util.go
Victor Lyuboslavsky 0180cc8086
Add SCEP endpoint for host identity. (#30589)
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 -->
2025-07-11 11:44:07 -03:00

107 lines
3 KiB
Go

package apple_mdm
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/binary"
"encoding/pem"
"fmt"
"math"
"net/url"
"path"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
)
// Note Apple rejects CSRs if the key size is not 2048.
const rsaKeySize = 2048
// newPrivateKey creates an RSA private key
func newPrivateKey() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, rsaKeySize)
}
func EncodeCertRequestPEM(cert *x509.CertificateRequest) []byte {
pemBlock := &pem.Block{
Type: "CERTIFICATE REQUEST",
Headers: nil,
Bytes: cert.Raw,
}
return pem.EncodeToMemory(pemBlock)
}
// GenerateRandomPin generates a `lenght`-digit PIN number that takes into
// account the current time as described in rfc4226 (for one time passwords)
//
// The implementation details have been mostly taken from https://github.com/pquerna/otp
func GenerateRandomPin(length int) string {
counter := uint64(time.Now().Unix()) //nolint:gosec // dismiss G115
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, counter)
m := sha256.New()
m.Write(buf)
sum := m.Sum(nil)
offset := sum[len(sum)-1] & 0xf
value := int64(((int(sum[offset]) & 0x7f) << 24) |
((int(sum[offset+1] & 0xff)) << 16) |
((int(sum[offset+2] & 0xff)) << 8) |
(int(sum[offset+3]) & 0xff))
v := int32(value % int64(math.Pow10(length))) //nolint:gosec // dismiss G115
f := fmt.Sprintf("%%0%dd", length)
return fmt.Sprintf(f, v)
}
// FmtErrorChain formats Command error message for macOS MDM v1
func FmtErrorChain(chain []mdm.ErrorChain) string {
var sb strings.Builder
for _, mdmErr := range chain {
desc := mdmErr.USEnglishDescription
if desc == "" {
desc = mdmErr.LocalizedDescription
}
sb.WriteString(fmt.Sprintf("%s (%d): %s\n", mdmErr.ErrorDomain, mdmErr.ErrorCode, desc))
}
return sb.String()
}
// FmtDDMError formats a DDM error message
func FmtDDMError(reasons []fleet.MDMAppleDDMStatusErrorReason) string {
var errMsg strings.Builder
for _, r := range reasons {
errMsg.WriteString(fmt.Sprintf("%s: %s %+v\n", r.Code, r.Description, r.Details))
}
return errMsg.String()
}
func EnrollURL(token string, appConfig *fleet.AppConfig) (string, error) {
enrollURL, err := url.Parse(appConfig.MDMUrl())
if err != nil {
return "", err
}
enrollURL.Path = path.Join(enrollURL.Path, EnrollPath)
q := enrollURL.Query()
q.Set("token", token)
enrollURL.RawQuery = q.Encode()
return enrollURL.String(), nil
}
// IsLessThanVersion returns true if the current version is less than the target version.
// If either version is invalid, an error is returned.
func IsLessThanVersion(current string, target string) (bool, error) {
cv, err := fleet.VersionToSemverVersion(current)
if err != nil {
return false, fmt.Errorf("invalid current version: %w", err)
}
tv, err := fleet.VersionToSemverVersion(target)
if err != nil {
return false, fmt.Errorf("invalid target version: %w", err)
}
return cv.LessThan(tv), nil
}