From 3af540e25640127040a5d7446476afa4a984ca30 Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Thu, 7 Nov 2024 19:52:44 +0000 Subject: [PATCH 01/11] Support GCP Secret Manager for signer --- cmd/gcp/main.go | 44 ++++--- cmd/gcp/secret_manager.go | 141 ++++++++++++++++++++++ config.go | 12 +- deployment/live/gcp/test/README.md | 16 ++- deployment/modules/gcp/storage/main.tf | 44 +++++++ deployment/modules/gcp/storage/outputs.tf | 10 ++ go.mod | 1 + go.sum | 2 + 8 files changed, 242 insertions(+), 28 deletions(-) create mode 100644 cmd/gcp/secret_manager.go diff --git a/cmd/gcp/main.go b/cmd/gcp/main.go index d3b04dd5..e7f8b5d8 100644 --- a/cmd/gcp/main.go +++ b/cmd/gcp/main.go @@ -27,7 +27,6 @@ import ( "syscall" "time" - "github.com/google/trillian/crypto/keys/pem" "github.com/google/trillian/monitoring/opencensus" "github.com/google/trillian/monitoring/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -50,25 +49,25 @@ var ( notAfterStart timestampFlag notAfterLimit timestampFlag - httpEndpoint = flag.String("http_endpoint", "localhost:6962", "Endpoint for HTTP (host:port).") - metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will be visible on --http_endpoint.") - tesseraDeadline = flag.Duration("tessera_deadline", time.Second*10, "Deadline for Tessera requests.") - maskInternalErrors = flag.Bool("mask_internal_errors", false, "Don't return error strings with Internal Server Error HTTP responses.") - tracing = flag.Bool("tracing", false, "If true opencensus Stackdriver tracing will be enabled. See https://opencensus.io/.") - tracingProjectID = flag.String("tracing_project_id", "", "project ID to pass to stackdriver. Can be empty for GCP, consult docs for other platforms.") - tracingPercent = flag.Int("tracing_percent", 0, "Percent of requests to be traced. Zero is a special case to use the DefaultSampler.") - origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.") - projectID = flag.String("project_id", "", "GCP ProjectID.") - bucket = flag.String("bucket", "", "Name of the bucket to store the log in.") - spannerDB = flag.String("spanner_db_path", "", "Spanner database path: projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") - spannerDedupDB = flag.String("spanner_dedup_db_path", "", "Spanner deduplication database path: projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") - rootsPemFile = flag.String("roots_pem_file", "", "Path to the file containing root certificates that are acceptable to the log. The certs are served through get-roots endpoint.") - rejectExpired = flag.Bool("reject_expired", false, "If true then the certificate validity period will be checked against the current time during the validation of submissions. This will cause expired certificates to be rejected.") - rejectUnexpired = flag.Bool("reject_unexpired", false, "If true then CTFE rejects certificates that are either currently valid or not yet valid.") - extKeyUsages = flag.String("ext_key_usages", "", "If set, will restrict the set of such usages that the server will accept. By default all are accepted. The values specified must be ones known to the x509 package.") - rejectExtensions = flag.String("reject_extension", "", "A list of X.509 extension OIDs, in dotted string form (e.g. '2.3.4.5') which, if present, should cause submissions to be rejected.") - privKey = flag.String("private_key", "", "Path to a private key .der file. Used to sign checkpoints and SCTs.") - privKeyPass = flag.String("password", "", "private_key password.") + httpEndpoint = flag.String("http_endpoint", "localhost:6962", "Endpoint for HTTP (host:port).") + metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will be visible on --http_endpoint.") + tesseraDeadline = flag.Duration("tessera_deadline", time.Second*10, "Deadline for Tessera requests.") + maskInternalErrors = flag.Bool("mask_internal_errors", false, "Don't return error strings with Internal Server Error HTTP responses.") + tracing = flag.Bool("tracing", false, "If true opencensus Stackdriver tracing will be enabled. See https://opencensus.io/.") + tracingProjectID = flag.String("tracing_project_id", "", "project ID to pass to stackdriver. Can be empty for GCP, consult docs for other platforms.") + tracingPercent = flag.Int("tracing_percent", 0, "Percent of requests to be traced. Zero is a special case to use the DefaultSampler.") + origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.") + projectID = flag.String("project_id", "", "GCP ProjectID.") + bucket = flag.String("bucket", "", "Name of the bucket to store the log in.") + spannerDB = flag.String("spanner_db_path", "", "Spanner database path: projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") + spannerDedupDB = flag.String("spanner_dedup_db_path", "", "Spanner deduplication database path: projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") + rootsPemFile = flag.String("roots_pem_file", "", "Path to the file containing root certificates that are acceptable to the log. The certs are served through get-roots endpoint.") + rejectExpired = flag.Bool("reject_expired", false, "If true then the certificate validity period will be checked against the current time during the validation of submissions. This will cause expired certificates to be rejected.") + rejectUnexpired = flag.Bool("reject_unexpired", false, "If true then CTFE rejects certificates that are either currently valid or not yet valid.") + extKeyUsages = flag.String("ext_key_usages", "", "If set, will restrict the set of such usages that the server will accept. By default all are accepted. The values specified must be ones known to the x509 package.") + rejectExtensions = flag.String("reject_extension", "", "A list of X.509 extension OIDs, in dotted string form (e.g. '2.3.4.5') which, if present, should cause submissions to be rejected.") + signerPublicKeySecretName = flag.String("signer_public_key_secret_name", "", "Public key secret name for checkpoints and SCTs signer. Format: projects/{projectId}/secrets/{secretName}/versions/{secretVersion}.") + signerPrivateKeySecretName = flag.String("signer_private_key_secret_name", "", "Private key secret name for checkpoints and SCTs signer. Format: projects/{projectId}/secrets/{secretName}/versions/{secretVersion}.") ) // nolint:staticcheck @@ -77,10 +76,9 @@ func main() { flag.Parse() ctx := context.Background() - // TODO(phboneff): move to something else, like KMS - signer, err := pem.ReadPrivateKeyFile(*privKey, *privKeyPass) + signer, err := NewSecretManagerSigner(ctx, *signerPublicKeySecretName, *signerPrivateKeySecretName) if err != nil { - klog.Exitf("Can't open key: %v", err) + klog.Exitf("Can't create secret manager signer: %v", err) } vCfg, err := sctfe.ValidateLogConfig(*origin, *projectID, *bucket, *spannerDB, *rootsPemFile, *rejectExpired, *rejectUnexpired, *extKeyUsages, *rejectExtensions, notAfterStart.t, notAfterLimit.t, signer) diff --git a/cmd/gcp/secret_manager.go b/cmd/gcp/secret_manager.go new file mode 100644 index 00000000..634550b7 --- /dev/null +++ b/cmd/gcp/secret_manager.go @@ -0,0 +1,141 @@ +// Copyright 2024 Google LLC. All Rights Reserved. +// +// 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 main + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "hash/crc32" + "io" + + secretmanager "cloud.google.com/go/secretmanager/apiv1" + "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" +) + +// Signer implements crypto.Signer using Google Cloud Secret Manager. +type Signer struct { + publicKey crypto.PublicKey + privateKey crypto.PrivateKey +} + +// Public returns the public key stored in the Signer object. +func (s *Signer) Public() crypto.PublicKey { + return s.publicKey +} + +// Sign signs digest with the private key stored in Google Cloud Secret Manager. +// Only crypto.SHA256 and ECDSA are supported. +func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + // Verify hash function and digest bytes length. + if opts == nil || opts.HashFunc() != crypto.SHA256 { + return nil, fmt.Errorf("unsupported hash func: %v", opts.HashFunc()) + } + if len(digest) != opts.HashFunc().Size() { + return nil, fmt.Errorf("digest bytes length %d does not match hash function bytes length %d", len(digest), opts.HashFunc().Size()) + } + + privateKey, ok := s.privateKey.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("the key stored in Secret Manager is not an ECDSA key") + } + + return ecdsa.SignASN1(rand, privateKey, digest) +} + +// NewSecretManagerSigner creates a new signer that uses the ECDSA P-256 key pair in +// Google Cloud Secret Manager for signing digests. +func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKeySecretName string) (*Signer, error) { + client, err := secretmanager.NewClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create secret manager client: %w", err) + } + defer client.Close() + + // Public Key + publicKeyRaw, err := accessSecretVersion(ctx, client, publicKeySecretName) + if err != nil { + return nil, err + } + pemBlock, rest := pem.Decode([]byte(publicKeyRaw)) + if pemBlock == nil { + return nil, errors.New("failed to decode PEM") + } + if len(rest) > 0 { + return nil, fmt.Errorf("extra data after decoding PEM: %v", rest) + } + var publicKey crypto.PublicKey + switch pemBlock.Type { + case "PUBLIC KEY": + publicKey, err = x509.ParsePKIXPublicKey(pemBlock.Bytes) + default: + return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type) + } + if err != nil { + return nil, err + } + + // Private Key + privateKeyRaw, err := accessSecretVersion(ctx, client, privateKeySecretName) + if err != nil { + return nil, err + } + pemBlock, rest = pem.Decode([]byte(privateKeyRaw)) + if pemBlock == nil { + return nil, errors.New("failed to decode PEM") + } + if len(rest) > 0 { + return nil, fmt.Errorf("extra data after decoding PEM: %v", rest) + } + var privateKey crypto.PrivateKey + switch pemBlock.Type { + case "EC PRIVATE KEY": + privateKey, err = x509.ParseECPrivateKey(pemBlock.Bytes) + default: + return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type) + } + if err != nil { + return nil, err + } + + return &Signer{ + publicKey: publicKey, + privateKey: privateKey, + }, nil +} + +func accessSecretVersion(ctx context.Context, client *secretmanager.Client, secretName string) ([]byte, error) { + resp, err := client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{ + Name: secretName, + }) + if err != nil { + return nil, fmt.Errorf("failed to access secret version: %w", err) + } + if resp.Name != secretName { + return nil, errors.New("request corrupted in-transit") + } + // Verify the data checksum. + crc32c := crc32.MakeTable(crc32.Castagnoli) + checksum := int64(crc32.Checksum(resp.Payload.Data, crc32c)) + if checksum != *resp.Payload.DataCrc32C { + return nil, errors.New("Data corruption detected.") + } + + return resp.Payload.Data, nil +} diff --git a/config.go b/config.go index 8e524c5b..5889a2b2 100644 --- a/config.go +++ b/config.go @@ -16,6 +16,7 @@ package sctfe import ( "crypto" + "crypto/ecdsa" "errors" "fmt" "strings" @@ -32,7 +33,6 @@ type ValidatedLogConfig struct { // is also its submission prefix, as per https://c2sp.org/static-ct-api. Origin string // Used to sign the checkpoint and SCTs. - // TODO(phboneff): check that this is RSA or ECDSA only. Signer crypto.Signer // If set, ExtKeyUsages will restrict the set of such usages that the // server will accept. By default all are accepted. The values specified @@ -90,6 +90,16 @@ func ValidateLogConfig(origin string, projectID string, bucket string, spannerDB return nil, errors.New("empty rootsPemFile") } + // Validate signer that only ECDSA is supported. + if signer == nil { + return nil, errors.New("empty signer") + } + switch keyType := signer.Public().(type) { + case *ecdsa.PublicKey: + default: + return nil, fmt.Errorf("unsupported key type: %v", keyType) + } + lExtKeyUsages := []string{} lRejectExtensions := []string{} if extKeyUsages != "" { diff --git a/deployment/live/gcp/test/README.md b/deployment/live/gcp/test/README.md index 8ba6c03f..0ee99587 100644 --- a/deployment/live/gcp/test/README.md +++ b/deployment/live/gcp/test/README.md @@ -38,6 +38,13 @@ Terraforming the project can be done by: 1. `cd` to the relevant directory for the environment to deploy/change (e.g. `ci`) 2. Run `terragrunt apply` +Store the Secret Manager resource ID of signer key pair into the environment variables: + +```sh +export SCTFE_SIGNER_ECDSA_P256_PUBLIC_KEY_ID=$(terragrunt output -raw ecdsa_p256_public_key_id) +export SCTFE_SIGNER_ECDSA_P256_PRIVATE_KEY_ID=$(terragrunt output -raw ecdsa_p256_private_key_id) +``` + ## Run the SCTFE ### With fake chains @@ -50,10 +57,10 @@ go run ./cmd/gcp/ \ --bucket=${GOOGLE_PROJECT}-${TESSERA_BASE_NAME}-bucket \ --spanner_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-db \ --spanner_dedup_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-dedup-db \ - --private_key=./testdata/ct-http-server.privkey.pem \ - --password=dirk \ --roots_pem_file=./testdata/fake-ca.cert \ - --origin=${TESSERA_BASE_NAME} + --origin=${TESSERA_BASE_NAME} \ + --signer_public_key_secret_name=${SCTFE_SIGNER_ECDSA_P256_PUBLIC_KEY_ID} \ + --signer_private_key_secret_name=${SCTFE_SIGNER_ECDSA_P256_PRIVATE_KEY_ID} ``` In a different terminal you can either mint and submit certificates manually, or @@ -131,7 +138,6 @@ go run ./client/ctclient get-roots --log_uri=${SRC_LOG_URI} --text=false > /tmp/ sed -i 's-""-"/tmp/hammercfg/roots.pem"-g' /tmp/hammercfg/hammer.cfg ``` - Run the SCTFE with the same roots: ```bash @@ -145,6 +151,8 @@ go run ./cmd/gcp/ \ --roots_pem_file=/tmp/hammercfg/roots.pem \ --origin=${TESSERA_BASE_NAME} \ --spanner_dedup_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-dedup-db \ + --signer_public_key_secret_name=${SCTFE_SIGNER_ECDSA_P256_PUBLIC_KEY_ID} \ + --signer_private_key_secret_name=${SCTFE_SIGNER_ECDSA_P256_PRIVATE_KEY_ID} \ -v=3 ``` diff --git a/deployment/modules/gcp/storage/main.tf b/deployment/modules/gcp/storage/main.tf index 61d2f1ce..224a1910 100644 --- a/deployment/modules/gcp/storage/main.tf +++ b/deployment/modules/gcp/storage/main.tf @@ -65,3 +65,47 @@ resource "google_spanner_database" "dedup_db" { "CREATE TABLE IDSeq (id INT64 NOT NULL, h BYTES(MAX) NOT NULL, idx INT64 NOT NULL,) PRIMARY KEY (id, h)", ] } + +# Secret Manager + +# ECDSA key with P256 elliptic curve +resource "tls_private_key" "sctfe-ecdsa-p256" { + algorithm = "ECDSA" + ecdsa_curve = "P256" +} + +resource "google_secret_manager_secret" "sctfe-ecdsa-p256-public-key" { + secret_id = "sctfe-ecdsa-p256-public-key" + + labels = { + label = "sctfe-public-key" + } + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "sctfe-ecdsa-p256-public-key" { + secret = google_secret_manager_secret.sctfe-ecdsa-p256-public-key.id + + secret_data = tls_private_key.sctfe-ecdsa-p256.public_key_pem +} + +resource "google_secret_manager_secret" "sctfe-ecdsa-p256-private-key" { + secret_id = "sctfe-ecdsa-p256-private-key" + + labels = { + label = "sctfe-private-key" + } + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "sctfe-ecdsa-p256-private-key" { + secret = google_secret_manager_secret.sctfe-ecdsa-p256-private-key.id + + secret_data = tls_private_key.sctfe-ecdsa-p256.private_key_pem +} diff --git a/deployment/modules/gcp/storage/outputs.tf b/deployment/modules/gcp/storage/outputs.tf index c2239766..8d9d8d0b 100644 --- a/deployment/modules/gcp/storage/outputs.tf +++ b/deployment/modules/gcp/storage/outputs.tf @@ -17,3 +17,13 @@ output "dedup_spanner_db" { description = "Dedup Spanner database" value = google_spanner_database.dedup_db } + +output "ecdsa_p256_public_key_id" { + description = "Signer public key (P256_SHA256)" + value = google_secret_manager_secret_version.sctfe-ecdsa-p256-public-key.id +} + +output "ecdsa_p256_private_key_id" { + description = "Signer private key (P256_SHA256)" + value = google_secret_manager_secret_version.sctfe-ecdsa-p256-private-key.id +} diff --git a/go.mod b/go.mod index 63725578..6124d1cf 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/transparency-dev/static-ct go 1.23.1 require ( + cloud.google.com/go/secretmanager v1.14.2 cloud.google.com/go/spanner v1.71.0 cloud.google.com/go/storage v1.46.0 github.com/golang/mock v1.6.0 diff --git a/go.sum b/go.sum index 3ac808c5..19ce166e 100644 --- a/go.sum +++ b/go.sum @@ -490,6 +490,8 @@ cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISI cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/secretmanager v1.14.2 h1:2XscWCfy//l/qF96YE18/oUaNJynAx749Jg3u0CjQr8= +cloud.google.com/go/secretmanager v1.14.2/go.mod h1:Q18wAPMM6RXLC/zVpWTlqq2IBSbbm7pKBlM3lCKsmjw= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= From 19c5877406048b762899b02ee8a908e80ee28213 Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Thu, 7 Nov 2024 20:13:44 +0000 Subject: [PATCH 02/11] Add terraform dependency lock file --- deployment/live/gcp/test/.terraform.lock.hcl | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 deployment/live/gcp/test/.terraform.lock.hcl diff --git a/deployment/live/gcp/test/.terraform.lock.hcl b/deployment/live/gcp/test/.terraform.lock.hcl new file mode 100644 index 00000000..c0334c4e --- /dev/null +++ b/deployment/live/gcp/test/.terraform.lock.hcl @@ -0,0 +1,41 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "6.1.0" + constraints = "6.1.0" + hashes = [ + "h1:okppWOAoIPz45VkydzAA74HRLgEKvP4CFXypPU228j8=", + "zh:2463510438c97c59e06ab1fb1ef76221c844abd1bc404c439401fc256e9928ab", + "zh:2afd9b76a81c51632bd54d3cc3bdc2685e8d89b8ace8ca7578b1ae42880228b5", + "zh:51e2fb64c7c8258ac0ec7315d488e5c655b392bf565f9bee2922ee72f6abfb90", + "zh:85aa39bad51132810ee6cd369f426614abff59cb0274fc737d087c17afa9b5ee", + "zh:989669bfed5ca7bf4d960eb9f27a62cbe2578ca2907da7c74fc93edae9a497fa", + "zh:a26665782e90ef3fd322d6a23a1de383c81ae93395e7c2bd9648a1aa85c69876", + "zh:d5e1b785b4c8569b91153eeba89280ffbbe7a0aaabb708833ada67544aeed057", + "zh:d748c69eab6acc4ab7ec369b3bd3ddd5d2e4120d99570743dafde74934959a20", + "zh:eb853ab5c4c0d3e536b8c77abf844b7893ac355967c95b6e0d39b12526e67989", + "zh:f4b50f0ae082412ba189041b6ac540523b7d6463905fed63be67eec03e1539b9", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f6e7adcfafe267d9c657a6c087388f7e0c1e3be4dc179a9a823f75c830a499b7", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.0.6" + hashes = [ + "h1:dYSb3V94K5dDMtrBRLPzBpkMTPn+3cXZ/kIJdtFL+2M=", + "zh:10de0d8af02f2e578101688fd334da3849f56ea91b0d9bd5b1f7a243417fdda8", + "zh:37fc01f8b2bc9d5b055dc3e78bfd1beb7c42cfb776a4c81106e19c8911366297", + "zh:4578ca03d1dd0b7f572d96bd03f744be24c726bfd282173d54b100fd221608bb", + "zh:6c475491d1250050765a91a493ef330adc24689e8837a0f07da5a0e1269e11c1", + "zh:81bde94d53cdababa5b376bbc6947668be4c45ab655de7aa2e8e4736dfd52509", + "zh:abdce260840b7b050c4e401d4f75c7a199fafe58a8b213947a258f75ac18b3e8", + "zh:b754cebfc5184873840f16a642a7c9ef78c34dc246a8ae29e056c79939963c7a", + "zh:c928b66086078f9917aef0eec15982f2e337914c5c4dbc31dd4741403db7eb18", + "zh:cded27bee5f24de6f2ee0cfd1df46a7f88e84aaffc2ecbf3ff7094160f193d50", + "zh:d65eb3867e8f69aaf1b8bb53bd637c99c6b649ba3db16ded50fa9a01076d1a27", + "zh:ecb0c8b528c7a619fa71852bb3fb5c151d47576c5aab2bf3af4db52588722eeb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} \ No newline at end of file From 3208d076ff79e83509e8fe687dfc14ca107d11ba Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 11:24:14 +0000 Subject: [PATCH 03/11] Add security warning to tls_private_key.sctfe-ecdsa-p256 resource --- deployment/modules/gcp/storage/main.tf | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/deployment/modules/gcp/storage/main.tf b/deployment/modules/gcp/storage/main.tf index 224a1910..09375735 100644 --- a/deployment/modules/gcp/storage/main.tf +++ b/deployment/modules/gcp/storage/main.tf @@ -68,7 +68,15 @@ resource "google_spanner_database" "dedup_db" { # Secret Manager -# ECDSA key with P256 elliptic curve +# ECDSA key with P256 elliptic curve. Do NOT use this in production environment. +# +# Security Notice +# The private key generated by this resource will be stored unencrypted in your +# Terraform state file. Use of this resource for production deployments is not +# recommended. Instead, generate a private key file outside of Terraform and +# distribute it securely to the system where Terraform will be run. +# +# See https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key. resource "tls_private_key" "sctfe-ecdsa-p256" { algorithm = "ECDSA" ecdsa_curve = "P256" From 9f83e961c9ec799c454c486b53fb032e4bab5746 Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 11:25:48 +0000 Subject: [PATCH 04/11] Rename `Signer` to `ECDSAWithSHA256Signer` --- cmd/gcp/secret_manager.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/gcp/secret_manager.go b/cmd/gcp/secret_manager.go index 634550b7..caf9be23 100644 --- a/cmd/gcp/secret_manager.go +++ b/cmd/gcp/secret_manager.go @@ -29,20 +29,20 @@ import ( "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" ) -// Signer implements crypto.Signer using Google Cloud Secret Manager. -type Signer struct { +// ECDSAWithSHA256Signer implements crypto.Signer using Google Cloud Secret Manager. +// Only crypto.SHA256 and ECDSA are supported. +type ECDSAWithSHA256Signer struct { publicKey crypto.PublicKey privateKey crypto.PrivateKey } // Public returns the public key stored in the Signer object. -func (s *Signer) Public() crypto.PublicKey { +func (s *ECDSAWithSHA256Signer) Public() crypto.PublicKey { return s.publicKey } // Sign signs digest with the private key stored in Google Cloud Secret Manager. -// Only crypto.SHA256 and ECDSA are supported. -func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (s *ECDSAWithSHA256Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { // Verify hash function and digest bytes length. if opts == nil || opts.HashFunc() != crypto.SHA256 { return nil, fmt.Errorf("unsupported hash func: %v", opts.HashFunc()) @@ -61,7 +61,7 @@ func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([] // NewSecretManagerSigner creates a new signer that uses the ECDSA P-256 key pair in // Google Cloud Secret Manager for signing digests. -func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKeySecretName string) (*Signer, error) { +func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKeySecretName string) (*ECDSAWithSHA256Signer, error) { client, err := secretmanager.NewClient(ctx) if err != nil { return nil, fmt.Errorf("failed to create secret manager client: %w", err) @@ -114,7 +114,7 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey return nil, err } - return &Signer{ + return &ECDSAWithSHA256Signer{ publicKey: publicKey, privateKey: privateKey, }, nil From 5ba49ca4cef0e5b2e5dffda01b171253c6739a4d Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 11:43:50 +0000 Subject: [PATCH 05/11] Wrap err with key secret name --- cmd/gcp/secret_manager.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/gcp/secret_manager.go b/cmd/gcp/secret_manager.go index caf9be23..21121d5a 100644 --- a/cmd/gcp/secret_manager.go +++ b/cmd/gcp/secret_manager.go @@ -71,7 +71,7 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey // Public Key publicKeyRaw, err := accessSecretVersion(ctx, client, publicKeySecretName) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to access public key secret (%s): %w", publicKeySecretName, err) } pemBlock, rest := pem.Decode([]byte(publicKeyRaw)) if pemBlock == nil { @@ -94,7 +94,7 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey // Private Key privateKeyRaw, err := accessSecretVersion(ctx, client, privateKeySecretName) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to access private key secret (%s): %w", privateKeySecretName, err) } pemBlock, rest = pem.Decode([]byte(privateKeyRaw)) if pemBlock == nil { From 59fc624eafda7039c0a96a44d7cdcde85a4812ed Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 11:50:26 +0000 Subject: [PATCH 06/11] Refactor `pem.Decode` --- cmd/gcp/secret_manager.go | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/cmd/gcp/secret_manager.go b/cmd/gcp/secret_manager.go index 21121d5a..dcf81241 100644 --- a/cmd/gcp/secret_manager.go +++ b/cmd/gcp/secret_manager.go @@ -69,18 +69,11 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey defer client.Close() // Public Key - publicKeyRaw, err := accessSecretVersion(ctx, client, publicKeySecretName) + var publicKey crypto.PublicKey + pemBlock, err := secretPEM(ctx, client, publicKeySecretName) if err != nil { - return nil, fmt.Errorf("failed to access public key secret (%s): %w", publicKeySecretName, err) - } - pemBlock, rest := pem.Decode([]byte(publicKeyRaw)) - if pemBlock == nil { - return nil, errors.New("failed to decode PEM") - } - if len(rest) > 0 { - return nil, fmt.Errorf("extra data after decoding PEM: %v", rest) + return nil, fmt.Errorf("failed to get public key secret PEM (%s): %w", publicKeySecretName, err) } - var publicKey crypto.PublicKey switch pemBlock.Type { case "PUBLIC KEY": publicKey, err = x509.ParsePKIXPublicKey(pemBlock.Bytes) @@ -92,18 +85,11 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey } // Private Key - privateKeyRaw, err := accessSecretVersion(ctx, client, privateKeySecretName) + var privateKey crypto.PrivateKey + pemBlock, err = secretPEM(ctx, client, privateKeySecretName) if err != nil { - return nil, fmt.Errorf("failed to access private key secret (%s): %w", privateKeySecretName, err) - } - pemBlock, rest = pem.Decode([]byte(privateKeyRaw)) - if pemBlock == nil { - return nil, errors.New("failed to decode PEM") + return nil, fmt.Errorf("failed to get private key secret PEM (%s): %w", privateKeySecretName, err) } - if len(rest) > 0 { - return nil, fmt.Errorf("extra data after decoding PEM: %v", rest) - } - var privateKey crypto.PrivateKey switch pemBlock.Type { case "EC PRIVATE KEY": privateKey, err = x509.ParseECPrivateKey(pemBlock.Bytes) @@ -120,7 +106,7 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey }, nil } -func accessSecretVersion(ctx context.Context, client *secretmanager.Client, secretName string) ([]byte, error) { +func secretPEM(ctx context.Context, client *secretmanager.Client, secretName string) (*pem.Block, error) { resp, err := client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{ Name: secretName, }) @@ -137,5 +123,13 @@ func accessSecretVersion(ctx context.Context, client *secretmanager.Client, secr return nil, errors.New("Data corruption detected.") } - return resp.Payload.Data, nil + pemBlock, rest := pem.Decode([]byte(resp.Payload.Data)) + if pemBlock == nil { + return nil, errors.New("failed to decode PEM") + } + if len(rest) > 0 { + return nil, fmt.Errorf("extra data after decoding PEM: %v", rest) + } + + return pemBlock, nil } From 6576b98e7dbf3bbf28e7460a43dfdd1d2dc03f0a Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 12:26:34 +0000 Subject: [PATCH 07/11] Fix readme after rebase --- deployment/live/gcp/test/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/deployment/live/gcp/test/README.md b/deployment/live/gcp/test/README.md index 0ee99587..8fa83388 100644 --- a/deployment/live/gcp/test/README.md +++ b/deployment/live/gcp/test/README.md @@ -146,8 +146,6 @@ go run ./cmd/gcp/ \ --project_id=${GOOGLE_PROJECT} \ --bucket=${GOOGLE_PROJECT}-${TESSERA_BASE_NAME}-bucket \ --spanner_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-db \ - --private_key=./testdata/ct-http-server.privkey.pem \ - --password=dirk \ --roots_pem_file=/tmp/hammercfg/roots.pem \ --origin=${TESSERA_BASE_NAME} \ --spanner_dedup_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-dedup-db \ From 7abb62b5696593b59768c03745c2c71d7e14a0ab Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 12:40:28 +0000 Subject: [PATCH 08/11] Fix TF resources naming convention --- deployment/modules/gcp/storage/main.tf | 18 +++++++++--------- deployment/modules/gcp/storage/outputs.tf | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/deployment/modules/gcp/storage/main.tf b/deployment/modules/gcp/storage/main.tf index 09375735..c6332e33 100644 --- a/deployment/modules/gcp/storage/main.tf +++ b/deployment/modules/gcp/storage/main.tf @@ -77,12 +77,12 @@ resource "google_spanner_database" "dedup_db" { # distribute it securely to the system where Terraform will be run. # # See https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key. -resource "tls_private_key" "sctfe-ecdsa-p256" { +resource "tls_private_key" "sctfe_ecdsa_p256" { algorithm = "ECDSA" ecdsa_curve = "P256" } -resource "google_secret_manager_secret" "sctfe-ecdsa-p256-public-key" { +resource "google_secret_manager_secret" "sctfe_ecdsa_p256_public_key" { secret_id = "sctfe-ecdsa-p256-public-key" labels = { @@ -94,13 +94,13 @@ resource "google_secret_manager_secret" "sctfe-ecdsa-p256-public-key" { } } -resource "google_secret_manager_secret_version" "sctfe-ecdsa-p256-public-key" { - secret = google_secret_manager_secret.sctfe-ecdsa-p256-public-key.id +resource "google_secret_manager_secret_version" "sctfe_ecdsa_p256_public_key" { + secret = google_secret_manager_secret.sctfe_ecdsa_p256_public_key.id - secret_data = tls_private_key.sctfe-ecdsa-p256.public_key_pem + secret_data = tls_private_key.sctfe_ecdsa_p256.public_key_pem } -resource "google_secret_manager_secret" "sctfe-ecdsa-p256-private-key" { +resource "google_secret_manager_secret" "sctfe_ecdsa_p256_private_key" { secret_id = "sctfe-ecdsa-p256-private-key" labels = { @@ -112,8 +112,8 @@ resource "google_secret_manager_secret" "sctfe-ecdsa-p256-private-key" { } } -resource "google_secret_manager_secret_version" "sctfe-ecdsa-p256-private-key" { - secret = google_secret_manager_secret.sctfe-ecdsa-p256-private-key.id +resource "google_secret_manager_secret_version" "sctfe_ecdsa_p256_private_key" { + secret = google_secret_manager_secret.sctfe_ecdsa_p256_private_key.id - secret_data = tls_private_key.sctfe-ecdsa-p256.private_key_pem + secret_data = tls_private_key.sctfe_ecdsa_p256.private_key_pem } diff --git a/deployment/modules/gcp/storage/outputs.tf b/deployment/modules/gcp/storage/outputs.tf index 8d9d8d0b..77658f4c 100644 --- a/deployment/modules/gcp/storage/outputs.tf +++ b/deployment/modules/gcp/storage/outputs.tf @@ -20,10 +20,10 @@ output "dedup_spanner_db" { output "ecdsa_p256_public_key_id" { description = "Signer public key (P256_SHA256)" - value = google_secret_manager_secret_version.sctfe-ecdsa-p256-public-key.id + value = google_secret_manager_secret_version.sctfe_ecdsa_p256_public_key.id } output "ecdsa_p256_private_key_id" { description = "Signer private key (P256_SHA256)" - value = google_secret_manager_secret_version.sctfe-ecdsa-p256-private-key.id + value = google_secret_manager_secret_version.sctfe_ecdsa_p256_private_key.id } From dd0f245fe18e5648570d0a67a24802a126957b1f Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 13:51:01 +0000 Subject: [PATCH 09/11] Fix `panic` when `opts` is `nil` --- cmd/gcp/secret_manager.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/gcp/secret_manager.go b/cmd/gcp/secret_manager.go index dcf81241..efd5e8f7 100644 --- a/cmd/gcp/secret_manager.go +++ b/cmd/gcp/secret_manager.go @@ -44,7 +44,10 @@ func (s *ECDSAWithSHA256Signer) Public() crypto.PublicKey { // Sign signs digest with the private key stored in Google Cloud Secret Manager. func (s *ECDSAWithSHA256Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { // Verify hash function and digest bytes length. - if opts == nil || opts.HashFunc() != crypto.SHA256 { + if opts == nil { + return nil, errors.New("opts cannot be nil") + } + if opts.HashFunc() != crypto.SHA256 { return nil, fmt.Errorf("unsupported hash func: %v", opts.HashFunc()) } if len(digest) != opts.HashFunc().Size() { From 84cf5c2062c26fb7f97db9bf551cef748ec12865 Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 14:04:26 +0000 Subject: [PATCH 10/11] Verify the correctness of the signer key pair --- cmd/gcp/secret_manager.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/gcp/secret_manager.go b/cmd/gcp/secret_manager.go index efd5e8f7..e181f2ac 100644 --- a/cmd/gcp/secret_manager.go +++ b/cmd/gcp/secret_manager.go @@ -103,6 +103,11 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey return nil, err } + // Verify the correctness of the signer key pair + if !privateKey.(*ecdsa.PrivateKey).PublicKey.Equal(publicKey) { + return nil, errors.New("signer key pair doesn't match") + } + return &ECDSAWithSHA256Signer{ publicKey: publicKey, privateKey: privateKey, From f882294118f4e1ecc57344135a08fe02add0f0d5 Mon Sep 17 00:00:00 2001 From: Roger Ng Date: Fri, 8 Nov 2024 14:35:26 +0000 Subject: [PATCH 11/11] Refactor `ECDSAWithSHA256Signer` to use `ecdsa.{Public,Private}Key` --- cmd/gcp/secret_manager.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/gcp/secret_manager.go b/cmd/gcp/secret_manager.go index e181f2ac..e916161b 100644 --- a/cmd/gcp/secret_manager.go +++ b/cmd/gcp/secret_manager.go @@ -32,8 +32,8 @@ import ( // ECDSAWithSHA256Signer implements crypto.Signer using Google Cloud Secret Manager. // Only crypto.SHA256 and ECDSA are supported. type ECDSAWithSHA256Signer struct { - publicKey crypto.PublicKey - privateKey crypto.PrivateKey + publicKey *ecdsa.PublicKey + privateKey *ecdsa.PrivateKey } // Public returns the public key stored in the Signer object. @@ -54,12 +54,7 @@ func (s *ECDSAWithSHA256Signer) Sign(rand io.Reader, digest []byte, opts crypto. return nil, fmt.Errorf("digest bytes length %d does not match hash function bytes length %d", len(digest), opts.HashFunc().Size()) } - privateKey, ok := s.privateKey.(*ecdsa.PrivateKey) - if !ok { - return nil, fmt.Errorf("the key stored in Secret Manager is not an ECDSA key") - } - - return ecdsa.SignASN1(rand, privateKey, digest) + return ecdsa.SignASN1(rand, s.privateKey, digest) } // NewSecretManagerSigner creates a new signer that uses the ECDSA P-256 key pair in @@ -86,16 +81,21 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey if err != nil { return nil, err } + var ecdsaPublicKey *ecdsa.PublicKey + ecdsaPublicKey, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("the public key stored in Secret Manager is not an ECDSA key") + } // Private Key - var privateKey crypto.PrivateKey + var ecdsaPrivateKey *ecdsa.PrivateKey pemBlock, err = secretPEM(ctx, client, privateKeySecretName) if err != nil { return nil, fmt.Errorf("failed to get private key secret PEM (%s): %w", privateKeySecretName, err) } switch pemBlock.Type { case "EC PRIVATE KEY": - privateKey, err = x509.ParseECPrivateKey(pemBlock.Bytes) + ecdsaPrivateKey, err = x509.ParseECPrivateKey(pemBlock.Bytes) default: return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type) } @@ -104,13 +104,13 @@ func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKey } // Verify the correctness of the signer key pair - if !privateKey.(*ecdsa.PrivateKey).PublicKey.Equal(publicKey) { + if !ecdsaPrivateKey.PublicKey.Equal(ecdsaPublicKey) { return nil, errors.New("signer key pair doesn't match") } return &ECDSAWithSHA256Signer{ - publicKey: publicKey, - privateKey: privateKey, + publicKey: ecdsaPublicKey, + privateKey: ecdsaPrivateKey, }, nil }