Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Commit

Permalink
add SMS signing keys at the DB layer (#1649)
Browse files Browse the repository at this point in the history
* add SMS signing keys at the DB layer

* review comments

* same treatment for destroy
  • Loading branch information
mikehelmick authored Jan 21, 2021
1 parent d35953f commit a0b72d0
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 47 deletions.
2 changes: 1 addition & 1 deletion pkg/controller/certapi/signers.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (c *Controller) getSignerForRealm(ctx context.Context, authApp *database.Au
}

// Relam has custom signing keys.
signingKey, err := realm.GetCurrentSigningKey(c.db)
signingKey, err := realm.CurrentSigningKey(c.db)
if err != nil || signingKey == nil {
return nil, fmt.Errorf("unable to find current signing key for realm: %v: %w", realm.Model.ID, err)
}
Expand Down
39 changes: 39 additions & 0 deletions pkg/database/managed_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package database

// ManagedKey is an interface that allows for a realm to manage signing keys
// for different purposes.
type ManagedKey interface {
// GetKID returns the public key version string
GetKID() string
// ManagedKeyID returns the reference to the key ID in the KMS.
ManagedKeyID() string
// IsActive() returns true if this key is active
IsActive() bool

SetManagedKeyID(keyID string)
SetActive(active bool)

// These are expected to be static across all instances of an implementing type.
Table() string
Purpose() string
}

// RealmManagedKey indicates that this key is owned by a realm.
type RealmManagedKey interface {
ManagedKey
SetRealmID(id uint)
}
22 changes: 22 additions & 0 deletions pkg/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -2038,6 +2038,28 @@ func (db *Database) Migrations(ctx context.Context) []*gormigrate.Migration {
return nil
},
},
{
ID: "00090-AddSMSSigningKeys",
Migrate: func(tx *gorm.DB) error {
return multiExec(tx,
`CREATE TABLE sms_signing_keys (
id BIGSERIAL,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
realm_id INTEGER NOT NULL,
key_id TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (id))`,
`CREATE INDEX idx_sms_signing_keys_realm ON sms_signing_keys (realm_id)`,
`CREATE UNIQUE INDEX uix_sms_signing_keys_active ON sms_signing_keys (realm_id, active) WHERE (active IS TRUE)`,
)
},
Rollback: func(tx *gorm.DB) error {
return multiExec(tx,
`DROP TABLE IF EXISTS sms_signing_keys`)
},
},
}
}

Expand Down
142 changes: 98 additions & 44 deletions pkg/database/realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,20 +692,31 @@ func (db *Database) AbusePreventionEnabledRealmIDs() ([]uint64, error) {
return ids, nil
}

// GetCurrentSigningKey returns the currently active signing key, the one marked
// CurrentSigningKey returns the currently active certificate signing key, the one marked
// active in the database. If there is more than one active, the most recently
// created one wins. Should not occur due to transactional update.
func (r *Realm) GetCurrentSigningKey(db *Database) (*SigningKey, error) {
func (r *Realm) CurrentSigningKey(db *Database) (*SigningKey, error) {
var signingKey SigningKey
if err := db.db.
Where("realm_id = ?", r.ID).
Where("active = ?", true).
Order("signing_keys.created_at DESC").
First(&signingKey).
Error; err != nil {
if IsNotFound(err) {
return nil, nil
}
return nil, fmt.Errorf("unable to find signing key: %w", err)
}
return &signingKey, nil
}

// CurrentSMSSigningKey returns the currently active SMS signing key, the one marked
// active in the database. If there is more than one active, the most recently
// created one wins. Should not occur due to transactional update.
func (r *Realm) CurrentSMSSigningKey(db *Database) (*SMSSigningKey, error) {
var signingKey SMSSigningKey
if err := db.db.
Where("realm_id = ?", r.ID).
Where("active = ?", true).
First(&signingKey).
Error; err != nil {
return nil, fmt.Errorf("unable to find signing key: %w", err)
}
return &signingKey, nil
Expand All @@ -715,39 +726,48 @@ func (r *Realm) GetCurrentSigningKey(db *Database) (*SigningKey, error) {
// and transactionally sets all other signing keys to inactive. It accepts the
// database primary key ID but returns the KID of the now-active key.
func (r *Realm) SetActiveSigningKey(db *Database, id uint) (string, error) {
var signingKey SigningKey
return r.setActiveManagedSigningKey(db, id, &SigningKey{})
}

// SetActiveSMSSigningKey sets a specific signing key to active=true for the realm,
// and transactionally sets all other signing keys to inactive. It accepts the
// database primary key ID but returns the KID of the now-active key.
func (r *Realm) SetActiveSMSSigningKey(db *Database, id uint) (string, error) {
return r.setActiveManagedSigningKey(db, id, &SMSSigningKey{})
}

func (r *Realm) setActiveManagedSigningKey(db *Database, id uint, signingKey RealmManagedKey) (string, error) {
if err := db.db.Transaction(func(tx *gorm.DB) error {
// Find the key that should be active - do this first to ensure that the
// provided PK id is actually valid.
if err := tx.
Set("gorm:query_option", "FOR UPDATE").
Table("signing_keys").
Table(signingKey.Table()).
Where("id = ?", id).
Where("realm_id = ?", r.ID).
First(&signingKey).
First(signingKey).
Error; err != nil {
if IsNotFound(err) {
return fmt.Errorf("key to activate does not exist")
return fmt.Errorf("%s key to activate does not exist", signingKey.Purpose())
}
return fmt.Errorf("failed to find newly active key: %w", err)
}

// Mark all other keys as inactive.
if err := tx.
Table("signing_keys").
Table(signingKey.Table()).
Where("realm_id = ?", r.ID).
Where("id != ?", id).
Where("deleted_at IS NULL").
Update(map[string]interface{}{"active": false, "updated_at": time.Now().UTC()}).
Error; err != nil {
return fmt.Errorf("failed to mark existing keys as inactive: %w", err)
return fmt.Errorf("failed to mark existing %s keys as inactive: %w", signingKey.Purpose(), err)
}

// Mark the active key as active.
signingKey.Active = true
if err := tx.Save(&signingKey).Error; err != nil {
return fmt.Errorf("failed to mark new key as active: %w", err)
signingKey.SetActive(true)
if err := tx.Save(signingKey).Error; err != nil {
return fmt.Errorf("failed to mark new %s key as active: %w", signingKey.Purpose(), err)
}
return nil
}); err != nil {
Expand All @@ -762,13 +782,26 @@ func (r *Realm) SetActiveSigningKey(db *Database, id uint) (string, error) {
func (r *Realm) ListSigningKeys(db *Database) ([]*SigningKey, error) {
var keys []*SigningKey
if err := db.db.
Model(r).
Model(&SigningKey{}).
Where("realm_id = ?", r.ID).
Order("signing_keys.created_at DESC").
Related(&keys).
Find(&keys).
Error; err != nil {
return nil, err
}
return keys, nil
}

// ListSMSSigningKeys returns the non-deleted signing keys for a realm
// ordered by created_at desc.
func (r *Realm) ListSMSSigningKeys(db *Database) ([]*SMSSigningKey, error) {
var keys []*SMSSigningKey
if err := db.db.
Model(&SMSSigningKey{}).
Where("realm_id = ?", r.ID).
Order("sms_signing_keys.created_at DESC").
Find(&keys).
Error; err != nil {
if IsNotFound(err) {
return keys, nil
}
return nil, err
}
return keys, nil
Expand Down Expand Up @@ -1287,12 +1320,26 @@ func (r *Realm) SigningKeyID() string {
return fmt.Sprintf("realm-%d", r.ID)
}

func (r *Realm) SMSSigningKeyID() string {
return fmt.Sprintf("realm-sms-%d", r.ID)
}

// CreateSigningKeyVersion creates a new signing key version on the key manager
// and saves a reference to the new key version in the database. If creating the
// key in the key manager fails, the database is not updated. However, if
// updating the signing key in the database fails, the key is NOT deleted from
// the key manager.
func (r *Realm) CreateSigningKeyVersion(ctx context.Context, db *Database) (string, error) {
return r.createdManagedSigningKey(ctx, db, r.SigningKeyID(), &SigningKey{})
}

// CreateSMSSigningKeyVersion creates a new SMS signing key versino on the key manager
// and saves a reference to the new key version in the database.
func (r *Realm) CreateSMSSigningKeyVersion(ctx context.Context, db *Database) (string, error) {
return r.createdManagedSigningKey(ctx, db, r.SMSSigningKeyID(), &SMSSigningKey{})
}

func (r *Realm) createdManagedSigningKey(ctx context.Context, db *Database, keyID string, signingKey RealmManagedKey) (string, error) {
manager := db.signingKeyManager
if manager == nil {
return "", ErrNoSigningKeyManager
Expand All @@ -1303,7 +1350,7 @@ func (r *Realm) CreateSigningKeyVersion(ctx context.Context, db *Database) (stri
return "", fmt.Errorf("missing CERTIFICATE_SIGNING_KEYRING")
}

name := r.SigningKeyID()
name := keyID
if name == "" {
return "", fmt.Errorf("missing key name")
}
Expand All @@ -1313,17 +1360,17 @@ func (r *Realm) CreateSigningKeyVersion(ctx context.Context, db *Database) (stri
// excessive costs.
var count int64
if err := db.db.
Table("signing_keys").
Table(signingKey.Table()).
Where("realm_id = ?", r.ID).
Where("deleted_at IS NULL").
Count(&count).
Error; err != nil {
if !IsNotFound(err) {
return "", fmt.Errorf("failed to count existing signing keys: %w", err)
return "", fmt.Errorf("failed to count existing %s signing keys: %w", signingKey.Purpose(), err)
}
}
if max := db.config.MaxCertificateSigningKeyVersions; count >= max {
return "", fmt.Errorf("too many available signing keys (maximum: %d)", max)
return "", fmt.Errorf("too many available %s signing keys (maximum: %d)", signingKey.Purpose(), max)
}

// Create the parent key - this interface does not return an error if the key
Expand All @@ -1342,35 +1389,35 @@ func (r *Realm) CreateSigningKeyVersion(ctx context.Context, db *Database) (stri
// Drop a log message for debugging.
db.logger.Debugw("provisioned new signing key for realm",
"realm_id", r.ID,
"purpose", signingKey.Purpose(),
"key_id", version)

// Save the reference to the key in the database. This is done in a
// transaction to avoid a race where keys are being created simultaneously and
// both are set to active.
var signingKey SigningKey
if err := db.db.Transaction(func(tx *gorm.DB) error {
// Look and see if there are existing signing keys for this realm. We do
// this to determine if the new key should be set to "active" automatically
// or if the user needs to take manual action to move the pointer.
var count int64
if err := tx.
Table("signing_keys").
Table(signingKey.Table()).
Where("realm_id = ?", r.ID).
Count(&count).
Error; err != nil {
if !IsNotFound(err) {
return fmt.Errorf("failed to check for existing keys: %w", err)
return fmt.Errorf("failed to check for existing %s signing keys: %w", signingKey.Purpose(), err)
}
}

// Create the new key.
signingKey.RealmID = r.ID
signingKey.KeyID = version
signingKey.Active = (count == 0)
signingKey.SetRealmID(r.ID)
signingKey.SetManagedKeyID(version)
signingKey.SetActive(count == 0)

// Save the key.
if err := tx.Save(&signingKey).Error; err != nil {
return fmt.Errorf("failed to save reference to signing key: %w", err)
if err := tx.Save(signingKey).Error; err != nil {
return fmt.Errorf("failed to save reference to %s signing key: %w", signingKey.Purpose(), err)
}
return nil
}); err != nil {
Expand All @@ -1384,46 +1431,53 @@ func (r *Realm) CreateSigningKeyVersion(ctx context.Context, db *Database) (stri
// and the key manager. ID is the primary key ID from the database. If the id
// does not exist, it does nothing.
func (r *Realm) DestroySigningKeyVersion(ctx context.Context, db *Database, id interface{}) error {
return r.destroyManagedSigningKey(ctx, db, id, &SigningKey{})
}

func (r *Realm) DestroySMSSigningKeyVersion(ctx context.Context, db *Database, id interface{}) error {
return r.destroyManagedSigningKey(ctx, db, id, &SMSSigningKey{})
}

func (r *Realm) destroyManagedSigningKey(ctx context.Context, db *Database, id interface{}, signingKey ManagedKey) error {
manager := db.signingKeyManager
if manager == nil {
return ErrNoSigningKeyManager
}

if err := db.db.Transaction(func(tx *gorm.DB) error {
// Load the signing key to ensure it actually exists.
var signingKey SigningKey
if err := tx.
Set("gorm:query_option", "FOR UPDATE").
Table("signing_keys").
Table(signingKey.Table()).
Where("id = ?", id).
Where("realm_id = ?", r.ID).
First(&signingKey).
First(signingKey).
Error; err != nil {
if IsNotFound(err) {
return nil
}
return fmt.Errorf("failed to load signing key: %w", err)
return fmt.Errorf("failed to load %s signing key: %w", signingKey.Purpose(), err)
}

if signingKey.Active {
return fmt.Errorf("cannot destroy active signing key")
if signingKey.IsActive() {
return fmt.Errorf("cannot destroy active %s signing key", signingKey.Purpose())
}

// Delete the signing key from the key manager - we want to do this in the
// transaction so, if it fails, we can rollback and try again.
if err := manager.DestroyKeyVersion(ctx, signingKey.KeyID); err != nil {
return fmt.Errorf("failed to destroy signing key in key manager: %w", err)
if err := manager.DestroyKeyVersion(ctx, signingKey.ManagedKeyID()); err != nil {
return fmt.Errorf("failed to destroy %s signing key in key manager: %w", signingKey.Purpose(), err)
}

// Successfully deleted from the key manager, now remove the record.
if err := tx.Delete(&signingKey).Error; err != nil {
return fmt.Errorf("successfully destroyed signing key in key manager, "+
"but failed to delete signing key from database: %w", err)
if err := tx.Delete(signingKey).Error; err != nil {
return fmt.Errorf("successfully destroyed %s signing key in key manager, "+
"but failed to delete signing key from database: %w", signingKey.Purpose(), err)
}

return nil
}); err != nil {
return fmt.Errorf("failed to destroy signing key version: %w", err)
return fmt.Errorf("failed to destroy %s signing key version: %w", signingKey.Purpose(), err)
}

return nil
Expand Down
Loading

0 comments on commit a0b72d0

Please sign in to comment.