mirror of
https://github.com/fleetdm/fleet
synced 2026-05-18 14:38:53 +00:00
fixes: #31390 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
258 lines
11 KiB
Go
258 lines
11 KiB
Go
package fleet
|
|
|
|
import (
|
|
"crypto/sha1" // nolint:gosec // used for compatibility with existing osquery certificates table schema
|
|
"crypto/x509"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type HostCertificateSource string
|
|
|
|
const (
|
|
SystemHostCertificate HostCertificateSource = "system"
|
|
UserHostCertificate HostCertificateSource = "user"
|
|
)
|
|
|
|
// IsValid returns true if the current host certificate source value is
|
|
// accepted, otherwise false.
|
|
func (s HostCertificateSource) IsValid() bool {
|
|
switch s {
|
|
case SystemHostCertificate, UserHostCertificate:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// HostCertificateRecord is the database model for a host certificate.
|
|
type HostCertificateRecord struct {
|
|
ID uint `json:"-" db:"id"`
|
|
HostID uint `json:"-" db:"host_id"`
|
|
|
|
// SHA1Sum is a SHA-1 hash of the DER encoded certificate.
|
|
SHA1Sum []byte `json:"-" db:"sha1_sum"`
|
|
|
|
// CreatedAt is the time the certificate was recorded by Fleet (i.e. certificate initially
|
|
// reported to Fleet).
|
|
CreatedAt time.Time `json:"-" db:"created_at"`
|
|
// DeletedAt is the time the certificate was soft deleted by Fleet (i.e. previously reported to
|
|
// Fleet certificate is subsequently not reported).
|
|
DeletedAt *time.Time `json:"-" db:"deleted_at"`
|
|
|
|
// The following fields are extracted from the certificate.
|
|
NotValidAfter time.Time `json:"-" db:"not_valid_after"`
|
|
NotValidBefore time.Time `json:"-" db:"not_valid_before"`
|
|
CertificateAuthority bool `json:"-" db:"certificate_authority"`
|
|
CommonName string `json:"-" db:"common_name"`
|
|
KeyAlgorithm string `json:"-" db:"key_algorithm"`
|
|
KeyStrength int `json:"-" db:"key_strength"`
|
|
KeyUsage string `json:"-" db:"key_usage"`
|
|
Serial string `json:"-" db:"serial"`
|
|
SigningAlgorithm string `json:"-" db:"signing_algorithm"`
|
|
|
|
SubjectCountry string `json:"-" db:"subject_country"`
|
|
SubjectOrganization string `json:"-" db:"subject_org"`
|
|
SubjectOrganizationalUnit string `json:"-" db:"subject_org_unit"`
|
|
SubjectCommonName string `json:"-" db:"subject_common_name"`
|
|
IssuerCountry string `json:"-" db:"issuer_country"`
|
|
IssuerOrganization string `json:"-" db:"issuer_org"`
|
|
IssuerOrganizationalUnit string `json:"-" db:"issuer_org_unit"`
|
|
IssuerCommonName string `json:"-" db:"issuer_common_name"`
|
|
|
|
Source HostCertificateSource `json:"-" db:"source"`
|
|
Username string `json:"-" db:"username"` // username that owns the certificate, only if source == 'user'
|
|
}
|
|
|
|
func NewHostCertificateRecord(
|
|
hostID uint,
|
|
cert *x509.Certificate,
|
|
) *HostCertificateRecord {
|
|
hash := sha1.Sum(cert.Raw) // nolint:gosec
|
|
|
|
return &HostCertificateRecord{
|
|
HostID: hostID,
|
|
SHA1Sum: hash[:], // nolint:gosec
|
|
NotValidAfter: cert.NotAfter,
|
|
NotValidBefore: cert.NotBefore,
|
|
CertificateAuthority: cert.IsCA,
|
|
// TODO: we need to define methodology for determining common name analogous to osquery,
|
|
// which seems to preferentially use Subject.CommonName for this value:
|
|
// https://github.com/osquery/osquery/blob/16bb01508eeca6d663b6d4f7e15034306be0fc3d/osquery/tables/system/posix/openssl_utils.cpp#L253
|
|
CommonName: cert.Subject.CommonName,
|
|
KeyAlgorithm: cert.PublicKeyAlgorithm.String(),
|
|
// TODO: we need to define methodology for determining key strength analogous to osquery,
|
|
// which describes this value as "Key size used for RSA/DSA, or curve name":
|
|
// https://github.com/osquery/osquery/blob/16bb01508eeca6d663b6d4f7e15034306be0fc3d/osquery/tables/system/posix/openssl_utils.cpp#L337
|
|
KeyStrength: 0, // TODO: add key strength here
|
|
// TODO: we need to define methodology for determining key usage analogous to osquery, which
|
|
// describes this as "Certificate key usage and extended key usage":
|
|
// https://github.com/osquery/osquery/blob/16bb01508eeca6d663b6d4f7e15034306be0fc3d/osquery/tables/system/posix/openssl_utils.cpp#L166
|
|
KeyUsage: "",
|
|
Serial: cert.SerialNumber.Text(16),
|
|
SigningAlgorithm: cert.SignatureAlgorithm.String(),
|
|
SubjectCommonName: cert.Subject.CommonName,
|
|
SubjectCountry: firstOrEmpty(cert.Subject.Country), // TODO: confirm methodology
|
|
SubjectOrganization: firstOrEmpty(cert.Subject.Organization), // TODO: confirm methodology
|
|
SubjectOrganizationalUnit: firstOrEmpty(cert.Subject.OrganizationalUnit), // TODO: confirm methodology
|
|
IssuerCommonName: cert.Issuer.CommonName,
|
|
IssuerCountry: firstOrEmpty(cert.Issuer.Country), // TODO: confirm methodology
|
|
IssuerOrganization: firstOrEmpty(cert.Issuer.Organization), // TODO: confirm methodology
|
|
IssuerOrganizationalUnit: firstOrEmpty(cert.Issuer.OrganizationalUnit), // TODO: confirm methodology
|
|
Source: SystemHostCertificate, // default to system host certificate, always 'system' for certs from MDM command for now
|
|
Username: "", // always empty since this is a system certificate
|
|
}
|
|
}
|
|
|
|
// ToPayload fills a HostCertificatePayload with the fields of a
|
|
// HostCertificateRecord. The HostCertificatePayload is used in API responses.
|
|
func (r *HostCertificateRecord) ToPayload() *HostCertificatePayload {
|
|
subject := &HostCertificateNameDetails{
|
|
CommonName: r.SubjectCommonName,
|
|
Country: r.SubjectCountry,
|
|
Organization: r.SubjectOrganization,
|
|
OrganizationalUnit: r.SubjectOrganizationalUnit,
|
|
}
|
|
issuer := &HostCertificateNameDetails{
|
|
CommonName: r.IssuerCommonName,
|
|
Country: r.IssuerCountry,
|
|
Organization: r.IssuerOrganization,
|
|
OrganizationalUnit: r.IssuerOrganizationalUnit,
|
|
}
|
|
return &HostCertificatePayload{
|
|
ID: r.ID,
|
|
NotValidAfter: r.NotValidAfter,
|
|
NotValidBefore: r.NotValidBefore,
|
|
CertificateAuthority: r.CertificateAuthority,
|
|
CommonName: r.CommonName,
|
|
KeyAlgorithm: r.KeyAlgorithm,
|
|
KeyStrength: r.KeyStrength,
|
|
KeyUsage: r.KeyUsage,
|
|
Serial: r.Serial,
|
|
SigningAlgorithm: r.SigningAlgorithm,
|
|
Source: r.Source,
|
|
Username: r.Username,
|
|
Subject: subject,
|
|
Issuer: issuer,
|
|
}
|
|
}
|
|
|
|
// HostCertificatePayload is the JSON model for API endpoints that return host certificates.
|
|
type HostCertificatePayload struct {
|
|
ID uint `json:"id"`
|
|
NotValidAfter time.Time `json:"not_valid_after"`
|
|
NotValidBefore time.Time `json:"not_valid_before"`
|
|
CertificateAuthority bool `json:"certificate_authority"`
|
|
CommonName string `json:"common_name"`
|
|
KeyAlgorithm string `json:"key_algorithm"`
|
|
KeyStrength int `json:"key_strength"`
|
|
KeyUsage string `json:"key_usage"`
|
|
Serial string `json:"serial"`
|
|
SigningAlgorithm string `json:"signing_algorithm"`
|
|
Source HostCertificateSource `json:"source"`
|
|
Username string `json:"username"`
|
|
|
|
Subject *HostCertificateNameDetails `json:"subject,omitempty"`
|
|
Issuer *HostCertificateNameDetails `json:"issuer,omitempty"`
|
|
}
|
|
|
|
type HostCertificateNameDetails struct {
|
|
CommonName string `json:"common_name"`
|
|
Country string `json:"country"`
|
|
Organization string `json:"organization"`
|
|
OrganizationalUnit string `json:"organizational_unit"`
|
|
}
|
|
|
|
// MDMAppleCertificateListResponse is the plist model for a certificate list response.
|
|
// https://developer.apple.com/documentation/devicemanagement/certificatelistresponse
|
|
type MDMAppleCertificateListResponse struct {
|
|
CertificateList []MDMAppleCertificateListItem `plist:"CertificateList"`
|
|
CommandUUID string `plist:"CommandUUID"`
|
|
EnrollmentID string `plist:"EnrollmentID"`
|
|
EnrollmentUserID string `plist:"EnrollmentUserID"`
|
|
ErrorChain []MDMAppleErrorChainItem `plist:"ErrorChain"`
|
|
NotOnConsole bool `plist:"NotOnConsole"`
|
|
Status string `plist:"Status"`
|
|
UDID string `plist:"UDID"`
|
|
UserID string `plist:"UserID"`
|
|
UserLongName string `plist:"UserLongName"`
|
|
UserShortName string `plist:"UserShortName"`
|
|
}
|
|
|
|
// MDMAppleCertificateListItem is the plist model for a certificate.
|
|
// https://developer.apple.com/documentation/devicemanagement/certificatelistresponse/certificatelistitem
|
|
type MDMAppleCertificateListItem struct {
|
|
CommonName string `plist:"CommonName"`
|
|
// Data is the DER-encoded certificate.
|
|
Data []byte `plist:"Data"`
|
|
IsIdentity bool `plist:"IsIdentity"`
|
|
}
|
|
|
|
func (c *MDMAppleCertificateListItem) Parse(hostID uint) (*HostCertificateRecord, error) {
|
|
cert, err := x509.ParseCertificate(c.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewHostCertificateRecord(hostID, cert), nil
|
|
}
|
|
|
|
// MdmAppleErrorChainItem is the plist model for an error chain item.
|
|
// https://developer.apple.com/documentation/devicemanagement/certificatelistresponse/errorchainitem
|
|
type MDMAppleErrorChainItem struct {
|
|
ErrorCode int `plist:"ErrorCode"`
|
|
ErrorDomain string `plist:"ErrorDomain"`
|
|
LocalizedDescription string `plist:"LocalizedDescription"`
|
|
USEnglishDescription string `plist:"USEnglishDescription"`
|
|
}
|
|
|
|
// ExtractDetailsFromOsqueryDistinguishedName parses a distinguished name and returns the country,
|
|
// organization, and organizational unit. It assumes provided string follows the formatting used by
|
|
// osquery `certificates` table[1], which appears to follow the style used by openSSL for `-subj`
|
|
// values). Key-value pairs are assumed to be separated by forward slashes, for example:
|
|
// "/C=US/O=Fleet Device Management Inc./OU=Fleet Device Management Inc./CN=FleetDM".
|
|
//
|
|
// See https://osquery.io/schema/5.15.0/#certificates
|
|
func ExtractDetailsFromOsqueryDistinguishedName(str string) (*HostCertificateNameDetails, error) {
|
|
str = strings.TrimSpace(str)
|
|
str = strings.Trim(str, "/")
|
|
|
|
str = strings.ReplaceAll(str, `\/`, `<<SLASH>>`) // Replace with our own "safe" sequence
|
|
parts := strings.Split(str, "/")
|
|
|
|
if len(parts) == 1 {
|
|
// Try to split into parts based on +
|
|
parts = strings.Split(str, "+")
|
|
}
|
|
|
|
var details HostCertificateNameDetails
|
|
for _, part := range parts {
|
|
key, value, found := strings.Cut(part, "=")
|
|
|
|
if !found {
|
|
return nil, fmt.Errorf("invalid distinguished name, wrong key value pair format: %s", str)
|
|
}
|
|
|
|
value = strings.ReplaceAll(strings.Trim(value, " "), `<<SLASH>>`, `/`) // Replace our "safe" sequence with forward slash
|
|
|
|
switch strings.ToUpper(key) {
|
|
case "C":
|
|
details.Country = strings.Trim(value, " ")
|
|
case "O":
|
|
details.Organization = strings.Trim(value, " ")
|
|
case "OU":
|
|
details.OrganizationalUnit = strings.Trim(value, " ")
|
|
case "CN":
|
|
details.CommonName = strings.Trim(value, " ")
|
|
}
|
|
}
|
|
|
|
return &details, nil
|
|
}
|
|
|
|
func firstOrEmpty(s []string) string {
|
|
if len(s) > 0 {
|
|
return s[0]
|
|
}
|
|
return ""
|
|
}
|