Skip to content

Commit

Permalink
jwk: Add ability to rotate SYSTEM_SECRET
Browse files Browse the repository at this point in the history
Closes #73

Signed-off-by: arekkas <aeneas@ory.am>
  • Loading branch information
arekkas committed Aug 27, 2018
1 parent 37c62b1 commit 2f26879
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 27 deletions.
61 changes: 57 additions & 4 deletions cmd/cli/handler_migrate.go
Expand Up @@ -78,8 +78,7 @@ func (h *MigrateHandler) connectToSql(dsn string) (*sqlx.DB, error) {
return db, nil
}

func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) {
var dburl string
func getDBUrl(cmd *cobra.Command, args []string, position int) (dburl string) {
if readFromEnv, _ := cmd.Flags().GetBool("read-from-env"); readFromEnv {
if len(viper.GetString("DATABASE_URL")) == 0 {
fmt.Println(cmd.UsageString())
Expand All @@ -89,11 +88,65 @@ func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) {
}
dburl = viper.GetString("DATABASE_URL")
} else {
if len(args) != 1 {
if len(args) < position {
fmt.Println(cmd.UsageString())
return
}
dburl = args[0]
dburl = args[position]
}
if dburl == "" {
fmt.Println(cmd.UsageString())
return
}
return
}

func (h *MigrateHandler) MigrateSecret(cmd *cobra.Command, args []string) {
dburl := getDBUrl(cmd, args, 0)
if dburl == "" {
return
}

db, err := h.connectToSql(dburl)
if err != nil {
fmt.Printf("An error occurred while connecting to SQL: %s", err)
os.Exit(1)
return
}

oldSecret := viper.GetString("OLD_SYSTEM_SECRET")
newSecret := viper.GetString("NEW_SYSTEM_SECRET")

if len(oldSecret) == 0 {
fmt.Println("You did not specify the old system secret, please set environment variable OLD_SYSTEM_SECRET.")
os.Exit(1)
return
} else if len(newSecret) == 0 {
fmt.Println("You did not specify the old system secret, please set environment variable NEW_SYSTEM_SECRET.")
os.Exit(1)
return
}

fmt.Println("Rotating encryption keys for JSON Web Key storage...")
manager := jwk.NewSQLManager(db, []byte(oldSecret))
if err := manager.RotateKeys(&jwk.AEAD{Key: []byte(newSecret)}); err != nil {
fmt.Printf("Error \"%s\" occurred while trying to rotate the JSON Web Key storage. All changes have been reverted.", err)
}
fmt.Println("Rotating encryption keys for JSON Web Key storage completed successfully!")
fmt.Printf(`You may now run ORY Hydra with the new system secret. If you wish that old OAuth 2.0 Access and Refres
tokens stay valid, please set environment variable ROTATED_SYSTEM_SECRET to the new secret:
ROTATED_SYSTEM_SECRET=%s hydra serve ...
If you wish that OAuth 2.0 Access and Refresh Tokens issued with the old secret are revoked, simply omit environment variable
ROTATED_SYSTEM_SECRET. This will NOT affect OpenID Connect ID Tokens!
`, newSecret)
}

func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) {
dburl := getDBUrl(cmd, args, 0)
if dburl == "" {
return
}

db, err := h.connectToSql(dburl)
Expand Down
6 changes: 0 additions & 6 deletions cmd/migrate.go
Expand Up @@ -21,19 +21,13 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

// migrateCmd represents the migrate command
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Various migration helpers",
Run: func(cmd *cobra.Command, args []string) {
// TODO: Work your own magic here
fmt.Print(cmd.UsageString())
},
}

func init() {
Expand Down
52 changes: 52 additions & 0 deletions cmd/migrate_secret.go
@@ -0,0 +1,52 @@
// Copyright © 2018 NAME HERE <EMAIL ADDRESS>
//
// 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 cmd

import (
"github.com/spf13/cobra"
)

// secretCmd represents the secret command
var secretCmd = &cobra.Command{
Use: "secret <database-url>",
Short: "Rotates system secrets",
Long: `This command rotates the system secret and reconfigures the store.
Example:
OLD_SYSTEM_SECRET=old-secret... NEW_SYSTEM_SECRET=new-secret... hydra migrate secret postgres://...
You can read in the database URL using the -e flag, for example:
export DATABASE_URL=...
hydra migrate secret^^ -e
`,
Run: cmdHandler.Migration.MigrateSecret,
}

func init() {
migrateCmd.AddCommand(secretCmd)

migrateSqlCmd.Flags().BoolP("read-from-env", "e", false, "If set, reads the database URL from the environment variable DATABASE_URL.")

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// secretCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// secretCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

}
5 changes: 5 additions & 0 deletions config/config.go
Expand Up @@ -59,6 +59,7 @@ type Config struct {
BackendBindHost string `mapstructure:"ADMIN_HOST" yaml:"-"`
Issuer string `mapstructure:"OAUTH2_ISSUER_URL" yaml:"-"`
SystemSecret string `mapstructure:"SYSTEM_SECRET" yaml:"-"`
RotatedSystemSecret string `mapstructure:"ROTATED_SYSTEM_SECRET" yaml:"-"`
DatabaseURL string `mapstructure:"DATABASE_URL" yaml:"-"`
DatabasePlugin string `mapstructure:"DATABASE_PLUGIN" yaml:"-"`
ConsentURL string `mapstructure:"OAUTH2_CONSENT_URL" yaml:"-"`
Expand Down Expand Up @@ -329,6 +330,10 @@ func (c *Config) GetCookieSecret() []byte {
return c.GetSystemSecret()
}

func (c *Config) GetRotatedSystemSecrets() [][]byte {
return [][]byte{[]byte(c.RotatedSystemSecret)}
}

func (c *Config) GetSystemSecret() []byte {
if len(c.systemSecret) > 0 {
return c.systemSecret
Expand Down
107 changes: 90 additions & 17 deletions jwk/manager_sql.go
Expand Up @@ -37,6 +37,10 @@ type SQLManager struct {
Cipher *AEAD
}

func NewSQLManager(db *sqlx.DB, key []byte) *SQLManager {
return &SQLManager{DB: db, Cipher: &AEAD{Key: key}}
}

var Migrations = &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Expand Down Expand Up @@ -119,20 +123,31 @@ func (m *SQLManager) AddKeySet(set string, keys *jose.JSONWebKeySet) error {
return errors.WithStack(err)
}

if err := m.addKeySet(tx, m.Cipher, set, keys); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}

if err := tx.Commit(); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}
return nil
}

func (m *SQLManager) addKeySet(tx *sqlx.Tx, cipher *AEAD, set string, keys *jose.JSONWebKeySet) error {
for _, key := range keys.Keys {
out, err := json.Marshal(key)
if err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return errors.WithStack(err)
}

encrypted, err := m.Cipher.Encrypt(out)
encrypted, err := cipher.Encrypt(out)
if err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return errors.WithStack(err)
}

Expand All @@ -142,19 +157,10 @@ func (m *SQLManager) AddKeySet(set string, keys *jose.JSONWebKeySet) error {
Version: 0,
Key: encrypted,
}); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}
}

if err := tx.Commit(); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}
return nil
}

Expand Down Expand Up @@ -218,7 +224,74 @@ func (m *SQLManager) DeleteKey(set, kid string) error {
}

func (m *SQLManager) DeleteKeySet(set string) error {
if _, err := m.DB.Exec(m.DB.Rebind(`DELETE FROM hydra_jwk WHERE sid=?`), set); err != nil {
tx, err := m.DB.Beginx()
if err != nil {
return errors.WithStack(err)
}

if err := m.deleteKeySet(tx, set); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}

if err := tx.Commit(); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}
return nil
}

func (m *SQLManager) deleteKeySet(tx *sqlx.Tx, set string) error {
if _, err := tx.Exec(m.DB.Rebind(`DELETE FROM hydra_jwk WHERE sid=?`), set); err != nil {
return sqlcon.HandleError(err)
}
return nil
}

func (m *SQLManager) RotateKeys(new *AEAD) error {
sids := make([]string, 0)
if err := m.DB.Select(&sids, "SELECT sid FROM hydra_jwk GROUP BY sid"); err != nil {
return sqlcon.HandleError(err)
}

sets := make([]jose.JSONWebKeySet, 0)
for _, sid := range sids {
set, err := m.GetKeySet(sid)
if err != nil {
return errors.WithStack(err)
}
sets = append(sets, *set)
}

tx, err := m.DB.Beginx()
if err != nil {
return errors.WithStack(err)
}

for k, set := range sets {
if err := m.deleteKeySet(tx, sids[k]); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}

if err := m.addKeySet(tx, new, sids[k], &set); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}
}

if err := tx.Commit(); err != nil {
if re := tx.Rollback(); re != nil {
return errors.Wrap(err, re.Error())
}
return sqlcon.HandleError(err)
}
return nil
Expand Down
36 changes: 36 additions & 0 deletions jwk/manager_test.go
Expand Up @@ -111,3 +111,39 @@ func TestManagerKeySet(t *testing.T) {
t.Run(fmt.Sprintf("case=%s", name), TestHelperManagerKeySet(m, ks, "TestManagerKeySet"))
}
}

func TestManagerRotate(t *testing.T) {
ks, err := testGenerator.Generate("TestManagerRotate", "sig")
require.NoError(t, err)

newKey, _ := RandomBytes(32)
newCipher := &AEAD{Key: newKey}

for name, m := range managers {
t.Run(fmt.Sprintf("manager=%s", name), func(t *testing.T) {
m, ok := m.(*SQLManager)
if !ok {
t.SkipNow()
}

n, err := m.CreateSchemas()
require.NoError(t, err)
t.Logf("Applied %d migrations to %s", n, name)

require.NoError(t, m.AddKeySet("TestManagerRotate", ks))

require.NoError(t, m.RotateKeys(newCipher))

_, err = m.GetKeySet("TestManagerRotate")
require.Error(t, err)

m.Cipher = newCipher
got, err := m.GetKeySet("TestManagerRotate")
require.NoError(t, err)

for _, key := range ks.Keys {
require.EqualValues(t, ks.Key(key.KeyID), got.Key(key.KeyID))
}
})
}
}

0 comments on commit 2f26879

Please sign in to comment.