Revise LUKS API to expect key slot and salt when escrowing successfully (#23952)

#23584

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
~~- [ ] Manual QA for all new/changed functionality~~ Will be tested E2E
This commit is contained in:
Ian Littman
2024-11-20 07:03:02 -06:00
committed by GitHub
parent f04375e670
commit d33d6f0bb2
10 changed files with 99 additions and 93 deletions
+8 -7
View File
@@ -3819,22 +3819,23 @@ ON DUPLICATE KEY UPDATE
return err
}
func (ds *Datastore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error {
if encryptedBase64Passphrase == "" { // should have been caught at service level
return errors.New("blank encrypted passphrase")
func (ds *Datastore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error {
if encryptedBase64Passphrase == "" || encryptedBase64Salt == "" { // should have been caught at service level
return errors.New("passphrase and salt must be set")
}
_, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_disk_encryption_keys
(host_id, base64_encrypted, base64_encrypted_slot_key, client_error, decryptable)
(host_id, base64_encrypted, base64_encrypted_salt, key_slot, client_error, decryptable)
VALUES
(?, ?, ?, '', TRUE)
(?, ?, ?, ?, '', TRUE)
ON DUPLICATE KEY UPDATE
decryptable = TRUE,
base64_encrypted = VALUES(base64_encrypted),
base64_encrypted_slot_key = VALUES(base64_encrypted_slot_key),
base64_encrypted_salt = VALUES(base64_encrypted_salt),
key_slot = VALUES(key_slot),
client_error = ''
`, hostID, encryptedBase64Passphrase, encryptedBase64SlotKey)
`, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot)
return err
}
func (ds *Datastore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool {
+10 -16
View File
@@ -7877,31 +7877,25 @@ func testLUKSDatastoreFunctions(t *testing.T, ds *Datastore) {
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID))
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID))
// no change when blank key attempted to save
err = ds.SaveLUKSData(ctx, host1.ID, "", "")
// no change when blank key or salt attempted to save
err = ds.SaveLUKSData(ctx, host1.ID, "", "", 0)
require.Error(t, err)
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
err = ds.SaveLUKSData(ctx, host1.ID, "foo", "", 0)
require.Error(t, err)
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
// persists with just passphrase
err = ds.SaveLUKSData(ctx, host1.ID, "foobar", "")
// persists with passphrase and salt set
err = ds.SaveLUKSData(ctx, host2.ID, "bazqux", "fuzzmuffin", 0)
require.NoError(t, err)
require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID))
key, err := ds.GetHostDiskEncryptionKey(ctx, host1.ID)
require.NoError(t, err)
require.Equal(t, "foobar", key.Base64Encrypted)
// persists with passphrase and slot key
err = ds.SaveLUKSData(ctx, host2.ID, "bazqux", "fuzzmuffin")
require.NoError(t, err)
require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID))
key, err = ds.GetHostDiskEncryptionKey(ctx, host2.ID)
key, err := ds.GetHostDiskEncryptionKey(ctx, host2.ID)
require.NoError(t, err)
require.Equal(t, "bazqux", key.Base64Encrypted)
// persists when host hasn't had anything queued
err = ds.SaveLUKSData(ctx, host3.ID, "newstuff", "")
err = ds.SaveLUKSData(ctx, host3.ID, "newstuff", "fuzzball", 1)
require.NoError(t, err)
require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID))
key, err = ds.GetHostDiskEncryptionKey(ctx, host3.ID)
@@ -0,0 +1,25 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20241116233322, Down_20241116233322)
}
func Up_20241116233322(tx *sql.Tx) error {
_, err := tx.Exec(`ALTER TABLE host_disk_encryption_keys
ADD COLUMN base64_encrypted_salt VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' AFTER base64_encrypted,
ADD COLUMN key_slot TINYINT UNSIGNED DEFAULT NULL AFTER base64_encrypted_salt`)
if err != nil {
return fmt.Errorf("failed to add base64_encrypted_salt and key_slot columns to host_disk_encryption_keys: %w", err)
}
return nil
}
func Down_20241116233322(tx *sql.Tx) error {
return nil
}
@@ -1,23 +0,0 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20241116233322, Down_20241116233322)
}
func Up_20241116233322(tx *sql.Tx) error {
_, err := tx.Exec(`ALTER TABLE host_disk_encryption_keys ADD COLUMN base64_encrypted_slot_key VARCHAR(255) NOT NULL DEFAULT '' AFTER base64_encrypted`)
if err != nil {
return fmt.Errorf("failed to add base64_encrypted_slot_key to host_disk_encryption_keys: %w", err)
}
return nil
}
func Down_20241116233322(tx *sql.Tx) error {
return nil
}
+2 -1
View File
@@ -303,7 +303,8 @@ CREATE TABLE `host_device_auth` (
CREATE TABLE `host_disk_encryption_keys` (
`host_id` int unsigned NOT NULL,
`base64_encrypted` text COLLATE utf8mb4_unicode_ci NOT NULL,
`base64_encrypted_slot_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
`base64_encrypted_salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
`key_slot` tinyint unsigned DEFAULT NULL,
`decryptable` tinyint(1) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+3 -2
View File
@@ -903,8 +903,9 @@ type Datastore interface {
// SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for
// a host
SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error
// SaveLUKSData sets base64'd encrypted LUKS passphrase and slot key data for a host
SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error
// SaveLUKSData sets base64'd encrypted LUKS passphrase, key slot, and salt data for a host that has successfully
// escrowed LUKS data
SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error
// GetUnverifiedDiskEncryptionKeys returns all the encryption keys that
// are collected but their decryptable status is not known yet (ie:
+1 -1
View File
@@ -394,7 +394,7 @@ type Service interface {
GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*MunkiIssue, error)
HostEncryptionKey(ctx context.Context, id uint) (*HostDiskEncryptionKey, error)
EscrowLUKSData(ctx context.Context, passphrase string, slotKey string, clientError string) error
EscrowLUKSData(ctx context.Context, passphrase string, salt string, keySlot *uint, clientError string) error
// AddLabelsToHost adds the given label names to the host's label membership.
//
+3 -3
View File
@@ -639,7 +639,7 @@ type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, e
type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error
type SaveLUKSDataFunc func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error
type SaveLUKSDataFunc func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error
type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error)
@@ -5048,11 +5048,11 @@ func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID
return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key, clientError, decryptable)
}
func (s *DataStore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error {
func (s *DataStore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error {
s.mu.Lock()
s.SaveLUKSDataFuncInvoked = true
s.mu.Unlock()
return s.SaveLUKSDataFunc(ctx, hostID, encryptedBase64Passphrase, encryptedBase64SlotKey)
return s.SaveLUKSDataFunc(ctx, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot)
}
func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
+17 -18
View File
@@ -1034,7 +1034,8 @@ func (svc *Service) SetOrUpdateDiskEncryptionKey(ctx context.Context, encryption
type orbitPostLUKSRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
Passphrase string `json:"passphrase"`
SlotKey string `json:"slot_key"`
Salt string `json:"salt"`
KeySlot *uint `json:"key_slot"`
ClientError string `json:"client_error"`
}
@@ -1057,13 +1058,13 @@ func (r orbitPostLUKSResponse) Status() int { return http.StatusNoContent }
func postOrbitLUKSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*orbitPostLUKSRequest)
if err := svc.EscrowLUKSData(ctx, req.Passphrase, req.SlotKey, req.ClientError); err != nil {
if err := svc.EscrowLUKSData(ctx, req.Passphrase, req.Salt, req.KeySlot, req.ClientError); err != nil {
return orbitPostLUKSResponse{Err: err}, nil
}
return orbitPostLUKSResponse{}, nil
}
func (svc *Service) EscrowLUKSData(ctx context.Context, passphrase string, slotKey string, clientError string) error {
func (svc *Service) EscrowLUKSData(ctx context.Context, passphrase string, salt string, keySlot *uint, clientError string) error {
// this is not a user-authenticated endpoint
svc.authz.SkipAuthorization(ctx)
@@ -1075,36 +1076,34 @@ func (svc *Service) EscrowLUKSData(ctx context.Context, passphrase string, slotK
if clientError != "" {
return svc.ds.ReportEscrowError(ctx, host.ID, clientError)
}
encryptedPassphrase, encryptedSlotKey, err := svc.validateAndEncrypt(ctx, passphrase, slotKey)
encryptedPassphrase, encryptedSalt, validatedKeySlot, err := svc.validateAndEncrypt(ctx, passphrase, salt, keySlot)
if err != nil {
_ = svc.ds.ReportEscrowError(ctx, host.ID, err.Error())
return err
}
return svc.ds.SaveLUKSData(ctx, host.ID, encryptedPassphrase, encryptedSlotKey)
return svc.ds.SaveLUKSData(ctx, host.ID, encryptedPassphrase, encryptedSalt, validatedKeySlot)
}
func (svc *Service) validateAndEncrypt(ctx context.Context, passphrase string, slotKey string) (string, string, error) {
if passphrase == "" {
return "", "", badRequest("Blank passphrase provided")
func (svc *Service) validateAndEncrypt(ctx context.Context, passphrase string, salt string, keySlot *uint) (encryptedPassphrase string, encryptedSalt string, validatedKeySlot uint, err error) {
if passphrase == "" || salt == "" || keySlot == nil {
return "", "", 0, badRequest("passphrase, salt, and key_slot must be provided to escrow LUKS data")
}
if svc.config.Server.PrivateKey == "" {
return "", "", newOsqueryError("internal error: missing server private key")
return "", "", 0, newOsqueryError("internal error: missing server private key")
}
encryptedPassphrase, err := mdm.EncryptAndEncode(passphrase, svc.config.Server.PrivateKey)
encryptedPassphrase, err = mdm.EncryptAndEncode(passphrase, svc.config.Server.PrivateKey)
if err != nil {
return "", "", ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data")
return "", "", 0, ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data")
}
var encryptedSlotKey string
if slotKey != "" {
encryptedSlotKey, err = mdm.EncryptAndEncode(slotKey, svc.config.Server.PrivateKey)
if err != nil {
return "", "", ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data")
}
encryptedSalt, err = mdm.EncryptAndEncode(salt, svc.config.Server.PrivateKey)
if err != nil {
return "", "", 0, ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data")
}
return encryptedPassphrase, encryptedSlotKey, nil
return encryptedPassphrase, encryptedSalt, *keySlot, nil
}
/////////////////////////////////////////////////////////////////////////////////
+30 -22
View File
@@ -180,20 +180,21 @@ func TestOrbitLUKSDataSave(t *testing.T) {
}
// test reporting client errors
err := svc.EscrowLUKSData(ctx, "foo", "bar", expectedErrorMessage)
err := svc.EscrowLUKSData(ctx, "foo", "bar", nil, expectedErrorMessage)
require.NoError(t, err)
require.True(t, ds.ReportEscrowErrorFuncInvoked)
// blank passphrase
ds.ReportEscrowErrorFuncInvoked = false
expectedErrorMessage = "Blank passphrase provided"
err = svc.EscrowLUKSData(ctx, "", "bar", "")
expectedErrorMessage = "passphrase, salt, and key_slot must be provided to escrow LUKS data"
err = svc.EscrowLUKSData(ctx, "", "bar", ptr.Uint(0), "")
require.Error(t, err)
require.True(t, ds.ReportEscrowErrorFuncInvoked)
ds.ReportEscrowErrorFuncInvoked = false
passphrase, slotKey := "foo", ""
ds.SaveLUKSDataFunc = func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error {
passphrase, salt := "foo", ""
var keySlot *uint
ds.SaveLUKSDataFunc = func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlotToPersist uint) error {
require.Equal(t, host.ID, hostID)
key := config.TestConfig().Server.PrivateKey
@@ -201,29 +202,36 @@ func TestOrbitLUKSDataSave(t *testing.T) {
require.NoError(t, err)
require.Equal(t, passphrase, decryptedPassphrase)
if encryptedBase64SlotKey == "" {
require.Equal(t, slotKey, encryptedBase64SlotKey)
return nil
}
decryptedSlotKey, err := mdm.DecodeAndDecrypt(encryptedBase64SlotKey, key)
decryptedSalt, err := mdm.DecodeAndDecrypt(encryptedBase64Salt, key)
require.NoError(t, err)
require.Equal(t, slotKey, decryptedSlotKey)
require.Equal(t, salt, decryptedSalt)
require.Equal(t, *keySlot, keySlotToPersist)
return nil
}
// with no slot key
err = svc.EscrowLUKSData(ctx, passphrase, slotKey, "")
// with no salt
err = svc.EscrowLUKSData(ctx, passphrase, salt, keySlot, "")
require.Error(t, err)
require.True(t, ds.ReportEscrowErrorFuncInvoked)
require.False(t, ds.SaveLUKSDataFuncInvoked)
// with no key slot
ds.ReportEscrowErrorFuncInvoked = false
salt = "baz"
err = svc.EscrowLUKSData(ctx, passphrase, salt, keySlot, "")
require.Error(t, err)
require.True(t, ds.ReportEscrowErrorFuncInvoked)
require.False(t, ds.SaveLUKSDataFuncInvoked)
// with salt and key slot
keySlot = ptr.Uint(0)
ds.ReportEscrowErrorFuncInvoked = false
err = svc.EscrowLUKSData(ctx, passphrase, salt, keySlot, "")
require.NoError(t, err)
require.False(t, ds.ReportEscrowErrorFuncInvoked)
require.True(t, ds.SaveLUKSDataFuncInvoked)
// with slot key
slotKey = "baz"
ds.SaveLUKSDataFuncInvoked = false
err = svc.EscrowLUKSData(ctx, passphrase, slotKey, "")
require.NoError(t, err)
require.True(t, ds.SaveLUKSDataFuncInvoked)
})
t.Run("fail when no/invalid private key is set", func(t *testing.T) {
@@ -243,7 +251,7 @@ func TestOrbitLUKSDataSave(t *testing.T) {
cfg.Server.PrivateKey = ""
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
ctx = test.HostContext(ctx, host)
err := svc.EscrowLUKSData(ctx, "foo", "bar", "")
err := svc.EscrowLUKSData(ctx, "foo", "bar", ptr.Uint(0), "")
require.Error(t, err)
require.True(t, ds.ReportEscrowErrorFuncInvoked)
@@ -252,7 +260,7 @@ func TestOrbitLUKSDataSave(t *testing.T) {
cfg.Server.PrivateKey = "invalid"
svc, ctx = newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
ctx = test.HostContext(ctx, host)
err = svc.EscrowLUKSData(ctx, "foo", "bar", "")
err = svc.EscrowLUKSData(ctx, "foo", "bar", ptr.Uint(0), "")
require.Error(t, err)
require.True(t, ds.ReportEscrowErrorFuncInvoked)
})