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

Add support for destroying signing key versions #389

Merged
merged 4 commits into from
Aug 27, 2020
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
78 changes: 52 additions & 26 deletions cmd/server/assets/realmkeys.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,40 @@
<div class="form-group row">
<label for="name" class="col-sm-3">Issuer (iss):</label>
<div class="input-group col-sm-9">
<input type="text" id="systemiss" class="form-control" value="{{.certIssuer}}" readonly/>
<input type="text" id="systemiss" class="form-control text-monospace" value="{{.systemCertIssuer}}" readonly/>
{{template "clippy" "systemiss"}}
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3">Audience (aud):</label>
<div class="input-group col-sm-9">
<input type="text" id="systemaud" class="form-control" value="{{.certAudience}}" readonly/>
<input type="text" id="systemaud" class="form-control text-monospace" value="{{.systemCertAudience}}" readonly/>
{{template "clippy" "systemaud"}}
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3">Certificate duration:</label>
<div class="input-group col-sm-9">
<input type="text" id="certDuration" class="form-control" value="{{.certDuration}}" readonly/>
<input type="text" id="certDuration" class="form-control text-monospace" value="{{.systemCertDuration}}" readonly/>
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3">Key ID (kid):</label>
<div class="input-group col-sm-9">
<input type="text" id="systemCertKeyID" class="form-control" value="{{.certKeyID}}" readonly/>
<input type="text" id="systemCertKeyID" class="form-control text-monospace" value="{{.systemCertKeyID}}" readonly/>
{{template "clippy" "systemCertKeyID"}}
</div>
</div>
<div class="form-group row">
<label for="name" class="col-sm-3">Public key:</label>
<div class="input-group col-sm-9">
<textarea class="form-control" rows="4" id="systemPublicKey" readonly>{{if .certPublicKey}}{{.certPublicKey}}{{else}}{{.certPublicKeyError}}{{end}}</textarea>
<textarea class="form-control text-monospace{{if .systemCertPublicKeyError}} is-invalid{{end}}" rows="4" id="systemPublicKey" readonly>{{.systemCertPublicKey}}</textarea>
{{template "clippy" "systemPublicKey"}}
{{if .systemCertPublicKeyError}}
<div class="invalid-feedback">
{{.systemCertPublicKeyError}}
</div>
{{end}}
</div>
</div>
</div>
Expand All @@ -58,8 +63,8 @@
{{ .csrfField }}
<div class="form-group row">
<div class="col-sm-12">
<span class="form-text text-muted">
You can rotate your verification certificate signing key by:
<span class="form-text">
<p>Rotate your verification certificate signing key by:</p>
<ol>
<li>Creating a new key.</li>
<li>Communicating that key version and public key to your <em>exposure notifications key server</em> operator.</li>
Expand All @@ -78,36 +83,57 @@
<table class="table table-bordered table-striped">
<thead>
<tr>
<th scope="col" width="30">KeyID</th>
<th scope="col">Public Key</th>
<th scope="col" width="30">Controls</th>
<th scope="col" width="75">Key ID</th>
<th scope="col">Public key</th>
</tr>
</thead>
<tbody>
{{$csrfField := .csrfField}}
{{$publicKeys := .publicKeys}}
{{range $rk := .realmKeys}}
<tr>
<td>{{$rk.GetKID}}
{{if $rk.Active}}<h4><span class="badge badge-success">Active</span></h4>{{end}}
<td>
<span class="text-monospace">{{$rk.GetKID}}</span>
{{if $rk.Active}}<span class="badge badge-success">Active</span>{{end}}
</td>
<td>
<div class="input-group">
<textarea class="form-control" rows="4" id="{{$rk.GetKID}}" readonly>{{index $publicKeys $rk.GetKID}}</textarea>
<textarea class="form-control text-monospace" rows="4" id="{{$rk.GetKID}}" readonly>{{index $publicKeys $rk.GetKID}}</textarea>
{{template "clippy" $rk.GetKID}}
</div>
Backed by (your server operator may ask you for this information):<br/>
<code>{{$rk.KeyID}}</code>
</td>
<td>

<p class="mt-3">Backed by:</p>
<div class="input-group">
<input type="text" id="key-{{$rk.ID}}" class="form-control text-monospace" value="{{$rk.KeyID}}" readonly/>
{{template "clippy" (printf "key-%d" $rk.ID)}}
</div>
<small class="form-text text-muted">
Your server operator may ask for this.
</small>

{{if not $rk.Active}}
<form method="POST" action="/realm/keys/activate">
{{ $csrfField }}
<input type="hidden" name="id" value="{{$rk.ID}}" />
<a href="#" class="btn btn-primary btn-block" data-confirm="Have you already shared the new certificate key version and public key with your 'key server' operator?" data-submit-form>Make Active</a>
</form>
<div class="row mt-3 align-items-end h-100">
<div class="col">
<a href="/realm/keys/{{$rk.ID}}"
class="text-danger"
data-method="DELETE"
data-confirm="Are you sure you want to destroy this key? This action is irreversible!"
data-toggle="tooltip"
title="Destroy this key version">
<span class="oi oi-trash" aria-hidden="true"></span>
</a>
</div>

<!-- TODO - implement destroy -->
<div class="col">
<form method="POST" action="/realm/keys/activate">
{{ $csrfField }}
<input type="hidden" name="id" value="{{$rk.ID}}" />
<a href="#" class="btn btn-primary float-right" data-confirm="Have you already shared the new certificate key version and public key with your 'key server' operator?" data-submit-form>
Activate
</a>
</form>
</div>
</div>
{{end}}
</td>
</tr>
Expand Down Expand Up @@ -180,7 +206,7 @@ <h1>Verification certificate key settings</h1>
<div class="form-group row">
<label for="name" class="col-sm-3">Issuer (iss):</label>
<div class="{{if $realm.UseRealmCertificateKey}}input-group{{end}} col-sm-9">
<input type="text" id="certificateIssuer" name="certificateIssuer" class="form-control" value="{{$realm.CertificateIssuer}}" {{if $realm.UseRealmCertificateKey}}readonly{{end}}/>
<input type="text" id="certificateIssuer" name="certificateIssuer" class="form-control text-monospace" value="{{$realm.CertificateIssuer}}" {{if $realm.UseRealmCertificateKey}}readonly{{end}}/>
{{if $realm.UseRealmCertificateKey}}
{{template "clippy" "certificateIssuer"}}
{{else}}
Expand All @@ -198,7 +224,7 @@ <h1>Verification certificate key settings</h1>
<div class="form-group row">
<label for="name" class="col-sm-3">Audience (aud):</label>
<div class="{{if $realm.UseRealmCertificateKey}}input-group{{end}} col-sm-9">
<input type="text" id="certificateAudience" name="certificateAudience" class="form-control" value="{{$realm.CertificateAudience}}" {{if $realm.UseRealmCertificateKey}}readonly{{end}}/>
<input type="text" id="certificateAudience" name="certificateAudience" class="form-control text-monospace" value="{{$realm.CertificateAudience}}" {{if $realm.UseRealmCertificateKey}}readonly{{end}}/>
{{if $realm.UseRealmCertificateKey}}
{{template "clippy" "certificateAudience"}}
{{else}}
Expand All @@ -217,7 +243,7 @@ <h1>Verification certificate key settings</h1>
<div class="form-group row">
<label for="name" class="col-sm-3">Certificate duration:</label>
<div class="col-sm-9">
<input type="text" id="certificateDuration" name="certificateDuration" class="form-control" value="{{$realm.CertificateDuration.Duration}}" />
<input type="text" id="certificateDuration" name="certificateDuration" class="form-control text-monospace" value="{{$realm.CertificateDuration.Duration}}" />
{{if $realm.ErrorsFor "certificateDuration"}}
<div class="invalid-feedback">
{{joinStrings ($realm.ErrorsFor "certificateDuration") ", "}}
Expand Down
10 changes: 9 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/google/exposure-notifications-verification-server/pkg/ratelimit/limitware"
"github.com/google/exposure-notifications-verification-server/pkg/render"

"github.com/google/exposure-notifications-server/pkg/keys"
"github.com/google/exposure-notifications-server/pkg/logging"
"github.com/google/exposure-notifications-server/pkg/observability"
"github.com/google/exposure-notifications-server/pkg/server"
Expand Down Expand Up @@ -114,6 +115,12 @@ func realMain(ctx context.Context) error {
}
defer db.Close()

// Setup signers
certificateSigner, err := keys.KeyManagerFor(ctx, &config.CertificateSigning.Keys)
if err != nil {
return fmt.Errorf("failed to create certificate key manager: %w", err)
}

// Setup firebase
app, err := firebase.NewApp(ctx, config.FirebaseConfig())
if err != nil {
Expand Down Expand Up @@ -302,11 +309,12 @@ func realMain(ctx context.Context) error {
realmSub.Handle("/settings", realmadminController.HandleIndex()).Methods("GET")
realmSub.Handle("/settings/save", realmadminController.HandleSave()).Methods("POST")

realmKeysController, err := realmkeys.New(ctx, config, db, h)
realmKeysController, err := realmkeys.New(ctx, config, db, certificateSigner, h)
if err != nil {
return fmt.Errorf("failed to create realmkeys controller: %w", err)
}
realmSub.Handle("/keys", realmKeysController.HandleIndex()).Methods("GET")
realmSub.Handle("/keys/{id}", realmKeysController.HandleDestroy()).Methods("DELETE")
realmSub.Handle("/keys/create", realmKeysController.HandleCreateKey()).Methods("POST")
realmSub.Handle("/keys/upgrade", realmKeysController.HandleUpgrade()).Methods("POST")
realmSub.Handle("/keys/save", realmKeysController.HandleSave()).Methods("POST")
Expand Down
51 changes: 51 additions & 0 deletions pkg/controller/realmkeys/destroy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2020 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 realmkeys

import (
"net/http"

"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/gorilla/mux"
)

func (c *Controller) HandleDestroy() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)

session := controller.SessionFromContext(ctx)
if session == nil {
controller.MissingSession(w, r, c.h)
return
}
flash := controller.Flash(session)

realm := controller.RealmFromContext(ctx)
if realm == nil {
controller.MissingRealm(w, r, c.h)
return
}

if err := realm.DestroySigningKeyVersion(ctx, c.db, vars["id"]); err != nil {
flash.Error("Failed to destroy signing key version: %v", err)
c.renderShow(ctx, w, r, realm)
return
}

flash.Alert("Successfully destroyed signing key!")
c.redirectShow(ctx, w, r)
})
}
9 changes: 8 additions & 1 deletion pkg/controller/realmkeys/realmkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"context"
"time"

"github.com/google/exposure-notifications-server/pkg/keys"
"github.com/google/exposure-notifications-server/pkg/logging"
"github.com/google/exposure-notifications-verification-server/pkg/config"
"github.com/google/exposure-notifications-verification-server/pkg/database"
Expand All @@ -33,9 +34,13 @@ type Controller struct {
h *render.Renderer
logger *zap.SugaredLogger
publicKeyCache *keyutils.PublicKeyCache

// systemCertificateKeyManager is the key manager used for system
// certificates. It is not used with per-realm keys.
systemCertificateKeyManager keys.KeyManager
}

func New(ctx context.Context, config *config.ServerConfig, db *database.Database, h *render.Renderer) (*Controller, error) {
func New(ctx context.Context, config *config.ServerConfig, db *database.Database, systemCertificationKeyManager keys.KeyManager, h *render.Renderer) (*Controller, error) {
logger := logging.FromContext(ctx)

publicKeyCache, err := keyutils.NewPublicKeyCache(time.Minute)
Expand All @@ -49,5 +54,7 @@ func New(ctx context.Context, config *config.ServerConfig, db *database.Database
h: h,
logger: logger,
publicKeyCache: publicKeyCache,

systemCertificateKeyManager: systemCertificationKeyManager,
}, nil
}
25 changes: 14 additions & 11 deletions pkg/controller/realmkeys/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ func (c *Controller) renderShow(ctx context.Context, w http.ResponseWriter, r *h

m["supportsPerRealmSigning"] = c.db.SupportsPerRealmSigning()
if c.db.SupportsPerRealmSigning() {
c.logger.Infof("listing signing keys")
keys, err := realm.ListSigningKeys(c.db)
c.logger.Infow("list result", "error", err, "keys", keys)
if err != nil {
controller.InternalError(w, r, c.h, err)
return
mikehelmick marked this conversation as resolved.
Show resolved Hide resolved
}

m["realmKeys"] = keys

publicKeys := make(map[string]string)
Expand Down Expand Up @@ -67,22 +67,25 @@ func (c *Controller) renderShow(ctx context.Context, w http.ResponseWriter, r *h
m["publicKeys"] = publicKeys
}

// Fallback to the system signing keys and present them in the UI.
if !realm.UseRealmCertificateKey {
// load the system information.
m["certIssuer"] = c.config.CertificateSigning.CertificateIssuer
m["certAudience"] = c.config.CertificateSigning.CertificateAudience
m["certDuration"] = c.config.CertificateSigning.CertificateDuration
m["certKeyID"] = c.config.CertificateSigning.CertificateSigningKeyID
signing := c.config.CertificateSigning

m["systemCertIssuer"] = signing.CertificateIssuer
m["systemCertAudience"] = signing.CertificateAudience
m["systemCertDuration"] = signing.CertificateDuration
m["systemCertKeyID"] = signing.CertificateSigningKeyID

// Download and PEM encode the public key.
publicKey, err := c.publicKeyCache.GetPublicKey(ctx, c.config.CertificateSigning.CertificateSigningKey, c.db.KeyManager())
publicKey, err := c.publicKeyCache.GetPublicKey(ctx, signing.CertificateSigningKey, c.systemCertificateKeyManager)
if err != nil {
m["certPublicKeyError"] = fmt.Sprintf("Error loading public key: %v", err)
m["systemCertPublicKeyError"] = fmt.Sprintf("Failed to load public key: %v", err)
} else {
pem, err := keyutils.EncodePublicKey(publicKey)
if err != nil {
m["certPublicKeyError"] = err.Error()
m["systemCertPublicKeyError"] = fmt.Sprintf("Failed to encode public key: %v", err)
} else {
m["certPublicKey"] = pem
m["systemCertPublicKey"] = pem
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions pkg/database/realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,14 @@ func (r *Realm) CreateSigningKeyVersion(ctx context.Context, db *Database) (stri
}

parent := db.config.CertificateSigningKeyRing
if parent == "" {
return "", fmt.Errorf("missing CERTIFICATE_SIGNING_KEYRING")
}

name := r.SigningKeyID()
if name == "" {
return "", fmt.Errorf("missing key name")
}

// Create the parent key - this interface does not return an error if the key
// already exists, so this is safe to run each time.
Expand Down Expand Up @@ -513,3 +520,52 @@ func (r *Realm) CreateSigningKeyVersion(ctx context.Context, db *Database) (stri

return signingKey.GetKID(), nil
}

// DestroySigningKeyVersion destroys the given key version in both the database
// 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 {
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").
Where("id = ?", id).
Where("realm_id = ?", r.ID).
First(&signingKey).
Error; err != nil {
if IsNotFound(err) {
return nil
}
return fmt.Errorf("failed to load signing key: %w", err)
}

if signingKey.Active {
return fmt.Errorf("cannot destroy active signing key")
}

// 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)
}

// 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)
}

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

return nil
}