Skip to content

Commit

Permalink
Use KMS keys to sign and verify. (#41)
Browse files Browse the repository at this point in the history
Use KMS keys and
[kmssigner](https://github.com/transparency-dev/armored-witness/tree/main/pkg/kmssigner)
instead of plaintext keys passed in as env vars.

This involves some changes to the args for calling the GCF functions to
specify the KMS key to be used.

Also fix some other sharp edges:
* If JSON decode fails (e.g. user sets key version to a string instead
of an integer), write the error to the HTTP response instead of just
logging it in Cloud Logging.
* In the Sequence function, compare paths of to-sequence file names only
after calling `filepath.Clean` on them.

Tested by deploying `integrate_dev` and `sequence_dev` GCF functions in
our Armored Witness GCP project.
  • Loading branch information
jiggoha authored Nov 13, 2023
1 parent f97ff79 commit 8fa8e49
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 107 deletions.
186 changes: 127 additions & 59 deletions experimental/gcp-log/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,59 +23,96 @@ import (
"fmt"
"net/http"
"os"

"github.com/transparency-dev/merkle/rfc6962"
"golang.org/x/mod/sumdb/note"
"google.golang.org/api/iterator"
"path/filepath"

"github.com/gcp_serverless_module/internal/storage"
"github.com/transparency-dev/serverless-log/pkg/log"

"cloud.google.com/go/kms/apiv1"
"github.com/transparency-dev/armored-witness/pkg/kmssigner"
fmtlog "github.com/transparency-dev/formats/log"
"github.com/transparency-dev/merkle/rfc6962"
"github.com/transparency-dev/serverless-log/pkg/log"
"golang.org/x/mod/sumdb/note"
"google.golang.org/api/iterator"
)

func validateCommonArgs(w http.ResponseWriter, origin string) (ok bool, pubKey string) {
if len(origin) == 0 {
type requestData struct {
// Common args.
Origin string `json:"origin"`
Bucket string `json:"bucket"`
NoteKeyName string `json:"noteKeyName"`
KMSKeyRing string `json:"kmsKeyRing"`
KMSKeyName string `json:"kmsKeyName"`
KMSKeyLocation string `json:"kmsKeyLocation"`
KMSKeyVersion uint `json:"kmsKeyVersion"`

// For Sequence requests.
EntriesDir string `json:"entriesDir"`

// For Integrate requests.
Initialise bool `json:"initialise"`
}

func validateCommonArgs(w http.ResponseWriter, d requestData) (ok bool) {
if len(d.Origin) == 0 {
http.Error(w, "Please set `origin` in HTTP body to log identifier.", http.StatusBadRequest)
return false, ""
return false
}

pubKey = os.Getenv("SERVERLESS_LOG_PUBLIC_KEY")
if len(pubKey) == 0 {
http.Error(w,
"Please set SERVERLESS_LOG_PUBLIC_KEY environment variable",
if len(d.KMSKeyRing) == 0 {
http.Error(w, "Please set `kmsKeyRing` in HTTP body to the signing key's key ring.",
http.StatusBadRequest)
return false
}
if len(d.KMSKeyName) == 0 {
http.Error(w, "Please set `kmsKeyName` in HTTP body to the signing key's name.",
http.StatusBadRequest)
return false
}
if len(d.KMSKeyLocation) == 0 {
http.Error(w, "Please set `kmsKeyLocation` in HTTP body to the signing key's location.",
http.StatusBadRequest)
return false
}
if d.KMSKeyVersion == 0 {
http.Error(w, "Please set `kmsKeyVersion` in HTTP body to the signing key's version as an integer.",
http.StatusBadRequest)
return false
}
if len(d.NoteKeyName) == 0 {
http.Error(w, "Please set `noteKeyName` in HTTP body to the key name for the note.",
http.StatusBadRequest)
return false, ""
return false
}

return true, pubKey
return true
}

// Sequence is the entrypoint of the `sequence` GCF function.
func Sequence(w http.ResponseWriter, r *http.Request) {
// TODO(jayhou): validate that EntriesDir is only touching the log path.

var d struct {
Bucket string `json:"bucket"`
EntriesDir string `json:"entriesDir"`
Origin string `json:"origin"`
}
// process request args

d := requestData{}
if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
code := http.StatusBadRequest
fmt.Printf("json.NewDecoder: %v", err)
http.Error(w, http.StatusText(code), code)
http.Error(w, fmt.Sprintf("Failed to decode JSON: %q", err), http.StatusBadRequest)
return
}

ok, pubKey := validateCommonArgs(w, d.Origin)
if !ok {
if ok := validateCommonArgs(w, d); !ok {
return
}
if len(d.EntriesDir) == 0 {
http.Error(w, fmt.Sprintf("Please set `entriesDir` in HTTP body to the "+
"prefix name of the GCS objects in the %q bucket to sequence.", d.Bucket),
http.StatusBadRequest)
return
}

// init storage

ctx := context.Background()
ctx := r.Context()
client, err := storage.NewClient(ctx, os.Getenv("GCP_PROJECT"), d.Bucket)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create GCS client: %q", err), http.StatusInternalServerError)
Expand All @@ -84,19 +121,25 @@ func Sequence(w http.ResponseWriter, r *http.Request) {

// Read the current log checkpoint to retrieve next sequence number.

cpRaw, err := client.ReadCheckpoint(ctx)
cpBytes, err := client.ReadCheckpoint(ctx)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to read log checkpoint: %q", err), http.StatusInternalServerError)
return
}

// Check signatures
v, err := note.NewVerifier(pubKey)
// Setup KMS note signer and verifier.

kmClient, _, noteVerifier, err := setupKMS(ctx, w, os.Getenv("GCP_PROJECT"),
d.KMSKeyLocation, d.KMSKeyRing, d.KMSKeyName, d.KMSKeyVersion, d.NoteKeyName)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to instantiate Verifier: %q", err), http.StatusInternalServerError)
fmt.Println(err)
return
}
cp, _, _, err := fmtlog.ParseCheckpoint(cpRaw, d.Origin, v)
defer kmClient.Close()

// Check signatures

cp, _, _, err := fmtlog.ParseCheckpoint(cpBytes, d.Origin, noteVerifier)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to parse Checkpoint: %q", err), http.StatusInternalServerError)
return
Expand All @@ -119,11 +162,12 @@ func Sequence(w http.ResponseWriter, r *http.Request) {
return
}
// Skip this directory - only add files under it.
if attrs.Name == d.EntriesDir {
if filepath.Clean(attrs.Name) == filepath.Clean(d.EntriesDir) {
continue
}

bytes, err := client.GetObjectData(ctx, attrs.Name)
fmt.Printf("Sequencing object %q with content %q\n", attrs.Name, string(bytes))
if err != nil {
http.Error(w,
fmt.Sprintf("Failed to get data of object %q: %q", attrs.Name, err),
Expand Down Expand Up @@ -154,39 +198,69 @@ func Sequence(w http.ResponseWriter, r *http.Request) {
}
}

// setupKMS returns a KeyManagementClient, note signer, note verifier, and
// error. If this function does not return an error, the caller is responsible
// for calling Close() on the KeyManagementClient.
func setupKMS(ctx context.Context, w http.ResponseWriter, gcpProject, keyLocation, keyRing,
keyName string, keyVersion uint, noteKeyName string) (*kms.KeyManagementClient, note.Signer, note.Verifier, error) {
kmsKeyName := fmt.Sprintf(kmssigner.KeyVersionNameFormat, gcpProject,
keyLocation, keyRing, keyName, keyVersion)

kmClient, err := kms.NewKeyManagementClient(ctx)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return nil, nil, nil, fmt.Errorf("Failed to create KeyManagementClient: %q", err)
}

noteSigner, err := kmssigner.New(ctx, kmClient, kmsKeyName, noteKeyName)
if err != nil {
defer kmClient.Close()
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return nil, nil, nil, fmt.Errorf("Failed to instantiate signer: %q", err)
}

vkey, err := kmssigner.VerifierKeyString(ctx, kmClient, kmsKeyName, noteSigner.Name())
if err != nil {
defer kmClient.Close()
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return nil, nil, nil, fmt.Errorf("Failed to create verifier key string: %q", err)
}

noteVerifier, err := note.NewVerifier(vkey)
if err != nil {
defer kmClient.Close()
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return nil, nil, nil, fmt.Errorf("Failed to instantiate verifier: %q", err)
}

return kmClient, noteSigner, noteVerifier, nil
}

// Integrate is the entrypoint of the `integrate` GCF function.
func Integrate(w http.ResponseWriter, r *http.Request) {
var d struct {
Origin string `json:"origin"`
Initialise bool `json:"initialise"`
Bucket string `json:"bucket"`
}
// process request args

d := requestData{}
if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
fmt.Printf("json.NewDecoder: %v", err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
fmt.Sprintf("json.NewDecoder: %v\n", err)
http.Error(w, fmt.Sprintf("Failed to decode JSON: %q", err), http.StatusBadRequest)
return
}

ok, pubKey := validateCommonArgs(w, d.Origin)
if !ok {
if ok := validateCommonArgs(w, d); !ok {
return
}

privKey := os.Getenv("SERVERLESS_LOG_PRIVATE_KEY")
if len(privKey) == 0 {
http.Error(w,
"Please set SERVERLESS_LOG_PUBLIC_KEY environment variable",
http.StatusBadRequest)
}

s, err := note.NewSigner(privKey)
// Setup KMS note signer and verifier.
ctx := r.Context()
kmClient, noteSigner, noteVerifier, err := setupKMS(ctx, w, os.Getenv("GCP_PROJECT"),
d.KMSKeyLocation, d.KMSKeyRing, d.KMSKeyName, d.KMSKeyVersion, d.NoteKeyName)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to instantiate signer: %q", err), http.StatusInternalServerError)
fmt.Println(err)
return
}
defer kmClient.Close()

ctx := context.Background()
client, err := storage.NewClient(ctx, os.Getenv("GCP_PROJECT"), d.Bucket)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create GCS client: %v", err), http.StatusBadRequest)
Expand All @@ -204,7 +278,7 @@ func Integrate(w http.ResponseWriter, r *http.Request) {
cp := fmtlog.Checkpoint{
Hash: h.EmptyRoot(),
}
if err := signAndWrite(ctx, &cp, cpNote, s, client, d.Origin); err != nil {
if err := signAndWrite(ctx, &cp, cpNote, noteSigner, client, d.Origin); err != nil {
http.Error(w, fmt.Sprintf("Failed to sign: %q", err), http.StatusInternalServerError)
}
fmt.Fprintf(w, fmt.Sprintf("Initialised log at %s.", d.Bucket))
Expand All @@ -220,13 +294,7 @@ func Integrate(w http.ResponseWriter, r *http.Request) {
}

// Check signatures
v, err := note.NewVerifier(pubKey)
if err != nil {
http.Error(w,
fmt.Sprintf("Failed to instantiate Verifier: %q", err),
http.StatusInternalServerError)
}
cp, _, _, err := fmtlog.ParseCheckpoint(cpRaw, d.Origin, v)
cp, _, _, err := fmtlog.ParseCheckpoint(cpRaw, d.Origin, noteVerifier)
if err != nil {
http.Error(w,
fmt.Sprintf("Failed to open Checkpoint: %q", err),
Expand All @@ -245,7 +313,7 @@ func Integrate(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Nothing to integrate", http.StatusInternalServerError)
}

err = signAndWrite(ctx, newCp, cpNote, s, client, d.Origin)
err = signAndWrite(ctx, newCp, cpNote, noteSigner, client, d.Origin)
if err != nil {
http.Error(w,
fmt.Sprintf("Failed to sign: %q", err),
Expand Down
33 changes: 17 additions & 16 deletions experimental/gcp-log/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,41 @@ module github.com/gcp_serverless_module
go 1.20

require (
cloud.google.com/go/kms v1.15.5
cloud.google.com/go/storage v1.33.0
github.com/transparency-dev/armored-witness v0.0.0-20231106114509-3d1fed57e76e
github.com/transparency-dev/formats v0.0.0-20230928092353-f8ed364213f7
github.com/transparency-dev/merkle v0.0.2
github.com/transparency-dev/serverless-log v0.0.0-20231001212932-d1a42e72eef9
golang.org/x/mod v0.12.0
google.golang.org/api v0.143.0
k8s.io/klog/v2 v2.100.1
golang.org/x/mod v0.14.0
google.golang.org/api v0.149.0
k8s.io/klog/v2 v2.110.1
)

require (
cloud.google.com/go v0.110.7 // indirect
cloud.google.com/go/compute v1.23.0 // indirect
cloud.google.com/go v0.110.8 // indirect
cloud.google.com/go/compute v1.23.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.1 // indirect
github.com/go-logr/logr v1.2.0 // indirect
cloud.google.com/go/iam v1.1.3 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
google.golang.org/grpc v1.57.1 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
Loading

0 comments on commit 8fa8e49

Please sign in to comment.