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

Source token signing keys from the database #1602

Merged
merged 8 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/cleanup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func realMain(ctx context.Context) error {
}
tokenSignerTyp, ok := tokenSigner.(keys.SigningKeyManager)
if !ok {
return fmt.Errorf("token signing key manage is not a signing key manager (is %T)", tokenSigner)
return fmt.Errorf("token signing key manager is not a signing key manager (is %T)", tokenSigner)
}

// Create the router
Expand Down
2 changes: 1 addition & 1 deletion cmd/rotation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func realMain(ctx context.Context) error {
}
tokenSignerTyp, ok := tokenSigner.(keys.SigningKeyManager)
if !ok {
return fmt.Errorf("token signing key manage is not a signing key manager (is %T)", tokenSigner)
return fmt.Errorf("token signing key manager is not a signing key manager (is %T)", tokenSigner)
}

// Create the router
Expand Down
40 changes: 39 additions & 1 deletion cmd/server/assets/admin/info.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,46 @@
<dt>Build ID</dt>
<dd>{{.buildID}}</dd>

<dt>Build Tag</dt>
<dt>Build tag</dt>
<dd>{{.buildTag}}</dd>

<dt>Token signing keys</dt>
{{if $keys := .tokenSigningKeys}}
<dd>
<table class="small table table-bordered table-striped table-fixed mt-2">
<thead>
<tr>
<th width="40"></th>
<th width="305">Key ID (kid)</th>
<th>Key version</th>
<th width="165">Created at</th>
</tr>
</thead>
<tbody>
{{range $key := $keys}}
<tr>
<td class="text-center">
{{if $key.IsActive}}
<span class="oi oi-check" aria-hidden="true" data-toggle="tooltip" title="Active key"></span>
{{end}}
</td>
<td class="text-monospace user-select-all">
{{$key.UUID}}
</td>
<td class="text-monospace user-select-all">
{{$key.KeyVersionID}}
</td>
<td>
<span data-timestamp="{{$key.CreatedAt.Format "1/02/2006 3:04:05 PM UTC"}}">{{$key.CreatedAt.Format "2006-02-01 15:04"}}</span>
</td>
</tr>
{{end}}
</tbody>
</table>
</dd>
{{else}}
<dd>not configured</dd>
{{end}}
</dl>
</div>
</div>
Expand Down
20 changes: 18 additions & 2 deletions docs/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ There are two types of "users" for the system:
permissions in the realm determine their level of access. Most users will
only have permission to issue codes. However, some users will have control
over administering the realm, viewing statistics, inviting other realm
users, or updating realm settings. See the [realm admininistration
users, or updating realm settings. See the [realm administration
guide](realm-admin-guide.md) for more information.

When bootstrapping a new system, a default system administrator with the email
Expand Down Expand Up @@ -305,7 +305,23 @@ lifetime is short, it is probably safe to remove the key beyond 30 days.
If you are using Terraform, increment the `db_verification_code_hmac_count` by 1.


### Certificate and token signing keys
### Token signing keys

**Recommended frequency:** automatic

The system automatically rotates token signing keys every 30 days.

When bootstrapping a new system from scratch, it can take up to 5 minutes for
the initial token signing key to become available. To expedite this process, you
can manually invoke the `rotation` scheduler job:

```sh
gcloud scheduler jobs run "rotation" \
--project "${PROJECT_ID}"
```


### Certificate signing keys

**Recommended frequency:** on demand

Expand Down
27 changes: 7 additions & 20 deletions internal/envstest/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
package envstest

import (
"context"
"strings"
"testing"
"time"

"github.com/google/exposure-notifications-server/pkg/keys"
"github.com/google/exposure-notifications-server/pkg/server"

"github.com/google/exposure-notifications-verification-server/internal/project"
"github.com/google/exposure-notifications-verification-server/internal/routes"
"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/google/exposure-notifications-verification-server/pkg/config"
Expand Down Expand Up @@ -61,7 +60,7 @@ type APIServerConfigResponse struct {
func NewAPIServerConfig(tb testing.TB, testDatabaseInstance *database.TestInstance) *APIServerConfigResponse {
tb.Helper()

ctx := context.Background()
ctx := project.TestContext(tb)

harness := NewTestHarness(tb, testDatabaseInstance)

Expand All @@ -78,22 +77,10 @@ func NewAPIServerConfig(tb testing.TB, testDatabaseInstance *database.TestInstan
// Create the token key manager. We need both the signing key and the IDs, so
// we cannot use the helper here.
tokenSigningKey := keys.TestSigningKey(tb, harness.KeyManager)
tokenTyp, ok := harness.KeyManager.(keys.SigningKeyManager)
if !ok {
tb.Fatal("kms cannot manage signing keys")
}
tokenSigningKeyVersion, err := tokenTyp.CreateKeyVersion(ctx, tokenSigningKey)
if err != nil {
if _, err := harness.Database.RotateTokenSigningKey(ctx, certTyp, tokenSigningKey, database.SystemTest); err != nil {
tb.Fatal(err)
}

// Extract the kid from the key (this is a filesystem key).
parts := strings.Split(tokenSigningKeyVersion, "/")
if len(parts) == 0 {
tb.Fatalf("invalid signing key version %q", tokenSigningKeyVersion)
}
kid := parts[len(parts)-1]

// Create the config.
cfg := &config.APIServerConfig{
Database: *harness.DatabaseConfig,
Expand All @@ -109,8 +96,8 @@ func NewAPIServerConfig(tb testing.TB, testDatabaseInstance *database.TestInstan
},
TokenSigning: config.TokenSigningConfig{
Keys: *harness.KeyManagerConfig,
TokenSigningKeys: []string{tokenSigningKeyVersion},
TokenSigningKeyIDs: []string{kid},
TokenSigningKeys: []string{tokenSigningKey},
TokenSigningKeyIDs: []string{"v1"},
TokenIssuer: "test-iss",
},
RateLimit: *harness.RateLimiterConfig,
Expand All @@ -120,7 +107,7 @@ func NewAPIServerConfig(tb testing.TB, testDatabaseInstance *database.TestInstan
// Process the config - this simulates production setups and also ensures we
// get the defaults for any unset values.
emptyLookuper := envconfig.MapLookuper(nil)
if err := config.ProcessWith(context.Background(), cfg, emptyLookuper); err != nil {
if err := config.ProcessWith(project.TestContext(tb), cfg, emptyLookuper); err != nil {
tb.Fatal(err)
}

Expand All @@ -134,7 +121,7 @@ func NewAPIServerConfig(tb testing.TB, testDatabaseInstance *database.TestInstan
}

func (r *APIServerConfigResponse) NewServer(tb testing.TB) *APIServerResponse {
ctx := context.Background()
ctx := project.TestContext(tb)
mux, closer, err := routes.APIServer(ctx, r.Config, r.Database, r.Cacher, r.RateLimiter, r.KeyManager, r.KeyManager)
tb.Cleanup(closer)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions internal/envstest/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ func NewIntegrationSuite(tb testing.TB) *IntegrationSuite {
apiServerConfig := NewAPIServerConfig(tb, testDatabaseInstance)

// Point everything at the same database, cacher, and key manager.
apiServerConfig.Database = adminAPIServerConfig.Database
apiServerConfig.Cacher = adminAPIServerConfig.Cacher
apiServerConfig.RateLimiter = adminAPIServerConfig.RateLimiter
adminAPIServerConfig.Database = apiServerConfig.Database
adminAPIServerConfig.Cacher = apiServerConfig.Cacher
adminAPIServerConfig.RateLimiter = apiServerConfig.RateLimiter

db := adminAPIServerConfig.Database

Expand Down
5 changes: 1 addition & 4 deletions internal/routes/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,7 @@ func APIServer(
sub.Use(rateLimit)

// POST /api/verify
verifyapiController, err := verifyapi.New(ctx, cfg, db, h, tokenSigner)
if err != nil {
return nil, closer, fmt.Errorf("failed to create verify api controller: %w", err)
}
verifyapiController := verifyapi.New(cfg, db, cacher, tokenSigner, h)
sub.Handle("", verifyapiController.HandleVerify()).Methods("POST")
}

Expand Down
36 changes: 0 additions & 36 deletions pkg/config/api_server_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package config
import (
"context"
"fmt"
"sync"
"time"

"github.com/google/exposure-notifications-verification-server/pkg/cache"
Expand Down Expand Up @@ -57,10 +56,6 @@ type APIServerConfig struct {

// Rate limiting configuration
RateLimit ratelimit.Config

// cached allowed public keys
allowedTokenPublicKeys map[string]string
mu sync.RWMutex
}

// NewAPIServerConfig returns the environment config for the API server.
Expand All @@ -73,37 +68,6 @@ func NewAPIServerConfig(ctx context.Context) (*APIServerConfig, error) {
return &config, nil
}

// AllowedTokenPublicKeys returns a map of 'kid' to the KMS KeyID reference.
// This represents the keys that are allowed to be used to verify tokens,
// the TokenSigningKey/TokenSigningKeyID.
func (c *APIServerConfig) AllowedTokenPublicKeys() map[string]string {
result, err := func() (map[string]string, error) {
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.allowedTokenPublicKeys) > 0 {
return c.allowedTokenPublicKeys, nil
}
return nil, fmt.Errorf("missing")
}()
if err == nil {
return result
}

c.mu.Lock()
defer c.mu.Unlock()
// handle race condition that could occur between lock upgrade.
if len(c.allowedTokenPublicKeys) != 0 {
return c.allowedTokenPublicKeys
}

c.allowedTokenPublicKeys = make(map[string]string, len(c.TokenSigning.TokenSigningKeyIDs))

for i, kid := range c.TokenSigning.TokenSigningKeyIDs {
c.allowedTokenPublicKeys[kid] = c.TokenSigning.TokenSigningKeys[i]
}
return c.allowedTokenPublicKeys
}

func (c *APIServerConfig) Validate() error {
fields := []struct {
Var time.Duration
Expand Down
65 changes: 56 additions & 9 deletions pkg/config/token_signing_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package config

import (
"fmt"
"strings"

"github.com/google/exposure-notifications-server/pkg/keys"
)
Expand All @@ -25,33 +26,79 @@ import (
type TokenSigningConfig struct {
// Keys determines the key manager configuration for this token signing
// configuration.
Keys keys.Config `env:",prefix=TOKEN_"`
Keys keys.Config `env:", prefix=TOKEN_"`

TokenSigningKeys []string `env:"TOKEN_SIGNING_KEY, required"`
// TokenSigningKeys is the parent token signing key (not the actual signing
// version). It is an array for backwards-compatibility, but in practice it
// should only have one element.
//
// Previously it was a list of all possible signing key versions, but those
// have moved into the database.
//
// TODO(sethvargo): Convert to string in 0.22+.
sethvargo marked this conversation as resolved.
Show resolved Hide resolved
TokenSigningKeys []string `env:"TOKEN_SIGNING_KEY, required"`

// TokenSigningKeyIDs specifies the list of kids, corresponding to the
// TokenSigningKey
//
// TODO(sethvargo): Remove in 0.22+.
//
// Deprecated: moved into the database.
TokenSigningKeyIDs []string `env:"TOKEN_SIGNING_KEY_ID, default=v1"`
TokenIssuer string `env:"TOKEN_ISSUER, default=diagnosis-verification-example"`

// TokenIssuer is the `iss` field on the JWT.
TokenIssuer string `env:"TOKEN_ISSUER, default=diagnosis-verification-example"`
}

func (t *TokenSigningConfig) ActiveKey() string {
// ParentKeyName returns the name of the parent key.
func (t *TokenSigningConfig) ParentKeyName() string {
// Validation prevents this slice from being empty.
return t.TokenSigningKeys[0]
value := t.TokenSigningKeys[0]

// This is Google-specific, but that's the only platform where we can
// meaningfully detect this.
if idx := strings.Index(t.TokenSigningKeys[0], "/cryptoKeyVersions"); idx != -1 {
value = value[0:idx]
}

return value
}

func (t *TokenSigningConfig) ActiveKeyID() string {
// Validation prevents this slice from being empty.
return t.TokenSigningKeyIDs[0]
// FindKeyByKid attempts to find the matching signing key for the given kid. The
// boolean indicates whether the search was successful.
//
// TODO(sethvargo): remove in 0.22+.
func (t *TokenSigningConfig) FindKeyByKid(kid string) (string, bool) {
idx := -1
for i, v := range t.TokenSigningKeyIDs {
if v == kid {
idx = i
break
}
}

if idx == -1 {
return "", false
}

// This is safe to index because the validation check ensures the lengths are
// equal.
return t.TokenSigningKeys[idx], true
}

// Validate validates the configuration.
func (t *TokenSigningConfig) Validate() error {
if len(t.TokenSigningKeys) == 0 {
return fmt.Errorf("TOKEN_SIGNING_KEY must have at least one entry")
return fmt.Errorf("TOKEN_SIGNING_KEY must have at least one element")
}

if len(t.TokenSigningKeyIDs) == 0 {
return fmt.Errorf("TOKEN_SIGNING_KEY_ID must have at least one entry")
}

if len(t.TokenSigningKeys) != len(t.TokenSigningKeyIDs) {
return fmt.Errorf("TOKEN_SIGNING_KEY and TOKEN_SIGNING_KEY_ID must be lists of the same length")
}

return nil
}
17 changes: 9 additions & 8 deletions pkg/controller/admin/caches.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ type cacheItem struct {
}

var caches = map[string]*cacheItem{
"apps:": {"Mobile apps", "Registered mobile apps for the redirector service"},
"authorized_apps:": {"API keys", "Authentication for API keys"},
"jwks:": {"JWKs", "JSON web key sets"},
"memberships:": {"Memberships", "All membership information"},
"public_keys:": {"Public keys", "PEM data from upstream key provider"},
"realms:": {"Realms", "All realm data"},
"stats:": {"Statistics", "API key, user, and realm statistics"},
"users:": {"Users", "All user data"},
"apps:": {"Mobile apps", "Registered mobile apps for the redirector service"},
"authorized_apps:": {"API keys", "Authentication for API keys"},
"jwks:": {"JWKs", "JSON web key sets"},
"memberships:": {"Memberships", "All membership information"},
"public_keys:": {"Public keys", "PEM data from upstream key provider"},
"realms:": {"Realms", "All realm data"},
"stats:": {"Statistics", "API key, user, and realm statistics"},
"token_signing_keys:": {"Token signing keys", "All token signing keys, including currently active"},
"users:": {"Users", "All user data"},
}

// HandleCachesIndex shows the caches page.
Expand Down
9 changes: 9 additions & 0 deletions pkg/controller/admin/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,16 @@ import (
func (c *Controller) HandleInfoShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

m := controller.TemplateMapFromContext(ctx)

tokenSigningKeys, err := c.db.ListTokenSigningKeys()
if err != nil {
controller.InternalError(w, r, c.h, err)
return
}
m["tokenSigningKeys"] = tokenSigningKeys

m.Title("Info - System Admin")
c.h.RenderHTML(w, "admin/info", m)
})
Expand Down
Loading