fleet/server/datastore/mysql/scep.go
Magnus Jensen d4f48b6f9c
ACME MDM -> main (#42926)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** The entire ACME feature branch merge

# 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.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

---------

Co-authored-by: Jordan Montgomery <elijah.jordan.montgomery@gmail.com>
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
Co-authored-by: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com>
2026-04-02 15:56:31 -05:00

106 lines
2.9 KiB
Go

package mysql
import (
"context"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"database/sql"
_ "embed"
"errors"
"fmt"
"math/big"
"github.com/fleetdm/fleet/v4/pkg/certificate"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/assets"
"github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
)
// SCEPDepot is a MySQL-backed SCEP certificate depot.
type SCEPDepot struct {
db *sql.DB
ds fleet.Datastore
}
var _ depot.Depot = (*SCEPDepot)(nil)
// newSCEPDepot creates and returns a *SCEPDepot.
func newSCEPDepot(db *sql.DB, ds fleet.Datastore) (*SCEPDepot, error) {
if err := db.Ping(); err != nil {
return nil, err
}
return &SCEPDepot{
db: db,
ds: ds,
}, nil
}
// CA returns the CA's certificate and private key.
func (d *SCEPDepot) CA(_ []byte) ([]*x509.Certificate, *rsa.PrivateKey, error) {
// TODO(roberto): nano interfaces doesn't receive a context for this method.
cert, err := assets.CAKeyPair(context.Background(), d.ds)
if err != nil {
return nil, nil, fmt.Errorf("getting assets: %w", err)
}
pk, ok := cert.PrivateKey.(*rsa.PrivateKey)
if !ok {
return nil, nil, errors.New("private key not in RSA format")
}
return []*x509.Certificate{cert.Leaf}, pk, nil
}
// Serial allocates and returns a new (increasing) serial number.
func (d *SCEPDepot) Serial() (*big.Int, error) {
result, err := d.db.Exec(`INSERT INTO identity_serials () VALUES ();`)
if err != nil {
return nil, err
}
lid, err := result.LastInsertId()
if err != nil {
return nil, err
}
return big.NewInt(lid), nil
}
// HasCN returns whether the given certificate exists in the depot.
//
// TODO(lucas): Implement and use allowTime and revokeOldCertificate.
// - allowTime are the maximum days before expiration to allow clients to do certificate renewal.
// - revokeOldCertificate specifies whether to revoke the old certificate once renewed.
func (d *SCEPDepot) HasCN(cn string, allowTime int, cert *x509.Certificate, revokeOldCertificate bool) (bool, error) {
var ct int
row := d.db.QueryRow(`SELECT COUNT(*) FROM identity_certificates WHERE name = ?`, cn)
if err := row.Scan(&ct); err != nil {
return false, err
}
return ct >= 1, nil
}
// Put stores a certificate under the given name.
//
// If the provided certificate has empty crt.Subject.CommonName,
// then the hex sha256 of the crt.Raw is used as name.
func (d *SCEPDepot) Put(name string, crt *x509.Certificate) error {
if crt.Subject.CommonName == "" {
name = fmt.Sprintf("%x", sha256.Sum256(crt.Raw))
}
if !crt.SerialNumber.IsInt64() {
return errors.New("cannot represent serial number as int64")
}
certPEM := certificate.EncodeCertPEM(crt)
_, err := d.db.Exec(`
INSERT INTO identity_certificates
(serial, name, not_valid_before, not_valid_after, certificate_pem)
VALUES
(?, ?, ?, ?, ?)`,
crt.SerialNumber.Int64(),
name,
crt.NotBefore,
crt.NotAfter,
certPEM,
)
return err
}