Skip to content

Commit 4e5e0cb

Browse files
authored
Replace fake signer with AWS Secrets Manager signer (#214)
* Replace fake signer with AWS Secrets Manager signer * Update `aws_secretsmanager_secret.sctfe_ecdsa_p256_private_key` tag label * Update error log when AWS Secrets Manager cannot be created
1 parent ad6964b commit 4e5e0cb

File tree

11 files changed

+288
-28
lines changed

11 files changed

+288
-28
lines changed

cmd/aws/main.go

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ package main
1717

1818
import (
1919
"context"
20-
"crypto/x509"
21-
"encoding/pem"
2220
"flag"
2321
"fmt"
2422
"net/http"
@@ -32,7 +30,6 @@ import (
3230
"github.com/go-sql-driver/mysql"
3331
"github.com/prometheus/client_golang/prometheus/promhttp"
3432
sctfe "github.com/transparency-dev/static-ct"
35-
"github.com/transparency-dev/static-ct/internal/testdata"
3633
"github.com/transparency-dev/static-ct/storage"
3734
awsSCTFE "github.com/transparency-dev/static-ct/storage/aws"
3835
"github.com/transparency-dev/static-ct/storage/bbolt"
@@ -52,25 +49,27 @@ var (
5249
notAfterStart timestampFlag
5350
notAfterLimit timestampFlag
5451

55-
httpEndpoint = flag.String("http_endpoint", "localhost:6962", "Endpoint for HTTP (host:port).")
56-
metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will be visible on --http_endpoint.")
57-
httpDeadline = flag.Duration("http_deadline", time.Second*10, "Deadline for HTTP requests.")
58-
maskInternalErrors = flag.Bool("mask_internal_errors", false, "Don't return error strings with Internal Server Error HTTP responses.")
59-
origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.")
60-
bucket = flag.String("bucket", "", "Name of the bucket to store the log in.")
61-
dbName = flag.String("db_name", "", "AuroraDB name")
62-
dbHost = flag.String("db_host", "", "AuroraDB host")
63-
dbPort = flag.Int("db_port", 3306, "AuroraDB port")
64-
dbUser = flag.String("db_user", "", "AuroraDB user")
65-
dbPassword = flag.String("db_password", "", "AuroraDB password")
66-
dbMaxConns = flag.Int("db_max_conns", 0, "Maximum connections to the database, defaults to 0, i.e unlimited")
67-
dbMaxIdle = flag.Int("db_max_idle_conns", 2, "Maximum idle database connections in the connection pool, defaults to 2")
68-
dedupPath = flag.String("dedup_path", "", "Path to the deduplication database.")
69-
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.")
70-
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.")
71-
rejectUnexpired = flag.Bool("reject_unexpired", false, "If true then CTFE rejects certificates that are either currently valid or not yet valid.")
72-
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.")
73-
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.")
52+
httpEndpoint = flag.String("http_endpoint", "localhost:6962", "Endpoint for HTTP (host:port).")
53+
metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will be visible on --http_endpoint.")
54+
httpDeadline = flag.Duration("http_deadline", time.Second*10, "Deadline for HTTP requests.")
55+
maskInternalErrors = flag.Bool("mask_internal_errors", false, "Don't return error strings with Internal Server Error HTTP responses.")
56+
origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.")
57+
bucket = flag.String("bucket", "", "Name of the bucket to store the log in.")
58+
dbName = flag.String("db_name", "", "AuroraDB name")
59+
dbHost = flag.String("db_host", "", "AuroraDB host")
60+
dbPort = flag.Int("db_port", 3306, "AuroraDB port")
61+
dbUser = flag.String("db_user", "", "AuroraDB user")
62+
dbPassword = flag.String("db_password", "", "AuroraDB password")
63+
dbMaxConns = flag.Int("db_max_conns", 0, "Maximum connections to the database, defaults to 0, i.e unlimited")
64+
dbMaxIdle = flag.Int("db_max_idle_conns", 2, "Maximum idle database connections in the connection pool, defaults to 2")
65+
dedupPath = flag.String("dedup_path", "", "Path to the deduplication database.")
66+
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.")
67+
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.")
68+
rejectUnexpired = flag.Bool("reject_unexpired", false, "If true then CTFE rejects certificates that are either currently valid or not yet valid.")
69+
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.")
70+
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.")
71+
signerPublicKeySecretName = flag.String("signer_public_key_secret_name", "", "Public key secret name for checkpoints and SCTs signer")
72+
signerPrivateKeySecretName = flag.String("signer_private_key_secret_name", "", "Private key secret name for checkpoints and SCTs signer")
7473
)
7574

7675
// nolint:staticcheck
@@ -79,13 +78,10 @@ func main() {
7978
flag.Parse()
8079
ctx := context.Background()
8180

82-
// TODO: Replace the fake signer with AWS Secrets Manager Signer.
83-
block, _ := pem.Decode([]byte(testdata.DemoPublicKey))
84-
key, err := x509.ParsePKIXPublicKey(block.Bytes)
81+
signer, err := NewSecretsManagerSigner(ctx, *signerPublicKeySecretName, *signerPrivateKeySecretName)
8582
if err != nil {
86-
klog.Exitf("Can't parse public key: %v", err)
83+
klog.Exitf("Can't create AWS Secrets Manager signer: %v", err)
8784
}
88-
fakeSigner := testdata.NewSignerWithFixedSig(key, []byte("sig"))
8985

9086
chainValidationConfig := sctfe.ChainValidationConfig{
9187
RootsPEMFile: *rootsPemFile,
@@ -97,7 +93,7 @@ func main() {
9793
NotAfterLimit: notAfterLimit.t,
9894
}
9995

100-
logHandler, err := sctfe.NewLogHandler(ctx, *origin, fakeSigner, chainValidationConfig, newAWSStorage, *httpDeadline, *maskInternalErrors)
96+
logHandler, err := sctfe.NewLogHandler(ctx, *origin, signer, chainValidationConfig, newAWSStorage, *httpDeadline, *maskInternalErrors)
10197
if err != nil {
10298
klog.Exitf("Can't initialize CT HTTP Server: %v", err)
10399
}

cmd/aws/secrets_manager.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright 2025 The Tessera authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"crypto"
20+
"crypto/ecdsa"
21+
"crypto/x509"
22+
"encoding/pem"
23+
"errors"
24+
"fmt"
25+
"io"
26+
27+
"github.com/aws/aws-sdk-go-v2/aws"
28+
"github.com/aws/aws-sdk-go-v2/config"
29+
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
30+
)
31+
32+
// TODO: Move ECDSAWithSHA256Signer to internal signer package.
33+
// ECDSAWithSHA256Signer implements crypto.Signer using AWS Secrets Manager.
34+
// Only crypto.SHA256 and ECDSA are supported.
35+
type ECDSAWithSHA256Signer struct {
36+
publicKey *ecdsa.PublicKey
37+
privateKey *ecdsa.PrivateKey
38+
}
39+
40+
// Public returns the public key stored in the Signer object.
41+
func (s *ECDSAWithSHA256Signer) Public() crypto.PublicKey {
42+
return s.publicKey
43+
}
44+
45+
// Sign signs digest with the private key stored in AWS Secrets Manager.
46+
func (s *ECDSAWithSHA256Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
47+
// Verify hash function and digest bytes length.
48+
if opts == nil {
49+
return nil, errors.New("opts cannot be nil")
50+
}
51+
if opts.HashFunc() != crypto.SHA256 {
52+
return nil, fmt.Errorf("unsupported hash func: %v", opts.HashFunc())
53+
}
54+
if len(digest) != opts.HashFunc().Size() {
55+
return nil, fmt.Errorf("digest bytes length %d does not match hash function bytes length %d", len(digest), opts.HashFunc().Size())
56+
}
57+
58+
return ecdsa.SignASN1(rand, s.privateKey, digest)
59+
}
60+
61+
// NewSecretsManagerSigner creates a new signer that uses the ECDSA P-256 key pair in
62+
// AWS Secrets Manager for signing digests.
63+
func NewSecretsManagerSigner(ctx context.Context, publicKeySecretName, privateKeySecretName string) (*ECDSAWithSHA256Signer, error) {
64+
sdkConfig, err := config.LoadDefaultConfig(ctx)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to load default AWS configuration: %v", err)
67+
}
68+
69+
// Create Secrets Manager client
70+
client := secretsmanager.NewFromConfig(sdkConfig)
71+
72+
// Public Key
73+
var publicKey crypto.PublicKey
74+
pemBlock, err := secretPEM(ctx, client, publicKeySecretName)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to get public key secret PEM (%s): %w", publicKeySecretName, err)
77+
}
78+
switch pemBlock.Type {
79+
case "PUBLIC KEY":
80+
publicKey, err = x509.ParsePKIXPublicKey(pemBlock.Bytes)
81+
default:
82+
return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type)
83+
}
84+
if err != nil {
85+
return nil, err
86+
}
87+
var ecdsaPublicKey *ecdsa.PublicKey
88+
ecdsaPublicKey, ok := publicKey.(*ecdsa.PublicKey)
89+
if !ok {
90+
return nil, fmt.Errorf("the public key stored in Secret Manager is not an ECDSA key")
91+
}
92+
93+
// Private Key
94+
var ecdsaPrivateKey *ecdsa.PrivateKey
95+
pemBlock, err = secretPEM(ctx, client, privateKeySecretName)
96+
if err != nil {
97+
return nil, fmt.Errorf("failed to get private key secret PEM (%s): %w", privateKeySecretName, err)
98+
}
99+
switch pemBlock.Type {
100+
case "EC PRIVATE KEY":
101+
ecdsaPrivateKey, err = x509.ParseECPrivateKey(pemBlock.Bytes)
102+
default:
103+
return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type)
104+
}
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
// Verify the correctness of the signer key pair
110+
if !ecdsaPrivateKey.PublicKey.Equal(ecdsaPublicKey) {
111+
return nil, errors.New("signer key pair doesn't match")
112+
}
113+
114+
return &ECDSAWithSHA256Signer{
115+
publicKey: ecdsaPublicKey,
116+
privateKey: ecdsaPrivateKey,
117+
}, nil
118+
}
119+
120+
func secretPEM(ctx context.Context, client *secretsmanager.Client, secretName string) (*pem.Block, error) {
121+
input := &secretsmanager.GetSecretValueInput{
122+
SecretId: aws.String(secretName),
123+
}
124+
125+
result, err := client.GetSecretValue(ctx, input)
126+
if err != nil {
127+
// For a list of exceptions thrown, see
128+
// https://<<{{DocsDomain}}>>/secretsmanager/latest/apireference/API_GetSecretValue.html
129+
return nil, fmt.Errorf("failed to get secret value: %w", err)
130+
}
131+
if result.SecretString == nil {
132+
return nil, fmt.Errorf("secretString is nil for secret %s", secretName)
133+
}
134+
135+
var secretString string = *result.SecretString
136+
137+
pemBlock, rest := pem.Decode([]byte(secretString))
138+
if pemBlock == nil {
139+
return nil, errors.New("failed to decode PEM")
140+
}
141+
if len(rest) > 0 {
142+
return nil, fmt.Errorf("extra data after decoding PEM: %v", rest)
143+
}
144+
145+
return pemBlock, nil
146+
}

cmd/gcp/secret_manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
3030
)
3131

32+
// TODO: Move ECDSAWithSHA256Signer to internal signer package.
3233
// ECDSAWithSHA256Signer implements crypto.Signer using Google Cloud Secret Manager.
3334
// Only crypto.SHA256 and ECDSA are supported.
3435
type ECDSAWithSHA256Signer struct {

deployment/live/aws/test/.terraform.lock.hcl

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
terraform {
2+
required_providers {
3+
aws = {
4+
source = "hashicorp/aws"
5+
version = "5.92.0"
6+
}
7+
}
8+
}
9+
10+
# Configure the AWS Provider
11+
provider "aws" {
12+
region = var.region
13+
}
14+
15+
# Secrets Manager
16+
17+
# ECDSA key with P256 elliptic curve. Do NOT use this in production environment.
18+
#
19+
# Security Notice
20+
# The private key generated by this resource will be stored unencrypted in your
21+
# Terraform state file. Use of this resource for production deployments is not
22+
# recommended.
23+
#
24+
# See https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key.
25+
resource "tls_private_key" "sctfe_ecdsa_p256" {
26+
algorithm = "ECDSA"
27+
ecdsa_curve = "P256"
28+
}
29+
30+
resource "aws_secretsmanager_secret" "sctfe_ecdsa_p256_public_key" {
31+
name = "${var.base_name}-ecdsa-p256-public-key"
32+
33+
tags = {
34+
label = "tesseract-public-key"
35+
}
36+
}
37+
38+
resource "aws_secretsmanager_secret_version" "sctfe_ecdsa_p256_public_key" {
39+
secret_id = aws_secretsmanager_secret.sctfe_ecdsa_p256_public_key.id
40+
secret_string = tls_private_key.sctfe_ecdsa_p256.public_key_pem
41+
}
42+
43+
resource "aws_secretsmanager_secret" "sctfe_ecdsa_p256_private_key" {
44+
name = "${var.base_name}-ecdsa-p256-private-key"
45+
46+
tags = {
47+
label = "tesseract-private-key"
48+
}
49+
}
50+
51+
resource "aws_secretsmanager_secret_version" "sctfe_ecdsa_p256_private_key" {
52+
secret_id = aws_secretsmanager_secret.sctfe_ecdsa_p256_private_key.id
53+
secret_string = tls_private_key.sctfe_ecdsa_p256.private_key_pem
54+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
output "ecdsa_p256_public_key_id" {
2+
description = "Signer public key (P256_SHA256)"
3+
value = aws_secretsmanager_secret.sctfe_ecdsa_p256_public_key.name
4+
}
5+
6+
output "ecdsa_p256_public_key_data" {
7+
description = "Signer public key (P256_SHA256) data from secret manager"
8+
value = aws_secretsmanager_secret_version.sctfe_ecdsa_p256_public_key.secret_string
9+
sensitive = true
10+
}
11+
12+
output "ecdsa_p256_private_key_id" {
13+
description = "Signer private key (P256_SHA256)"
14+
value = aws_secretsmanager_secret.sctfe_ecdsa_p256_private_key.name
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
variable "base_name" {
2+
description = "Base name to use when naming resources"
3+
type = string
4+
}
5+
6+
variable "region" {
7+
description = "Region in which to create resources"
8+
type = string
9+
}

deployment/modules/aws/tesseract/test/main.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,10 @@ module "storage" {
1010
region = var.region
1111
ephemeral = var.ephemeral
1212
}
13+
14+
module "secretsmanager" {
15+
source = "../../secretsmanager"
16+
17+
base_name = var.base_name
18+
region = var.region
19+
}

deployment/modules/aws/tesseract/test/outputs.tf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,13 @@ output "rds_aurora_cluster_master_user_secret" {
1919
value = module.storage.rds_aurora_cluster_master_user_secret
2020
sensitive = true
2121
}
22+
23+
output "ecdsa_p256_public_key_id" {
24+
description = "Signer public key (P256_SHA256)"
25+
value = module.secretsmanager.ecdsa_p256_public_key_id
26+
}
27+
28+
output "ecdsa_p256_private_key_id" {
29+
description = "Signer private key (P256_SHA256)"
30+
value = module.secretsmanager.ecdsa_p256_private_key_id
31+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/aws/aws-sdk-go-v2 v1.36.3
1111
github.com/aws/aws-sdk-go-v2/config v1.29.9
1212
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2
13+
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2
1314
github.com/aws/smithy-go v1.22.3
1415
github.com/gdamore/tcell/v2 v2.8.1
1516
github.com/go-sql-driver/mysql v1.9.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91Liq
678678
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
679679
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4=
680680
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
681+
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2 h1:vlYXbindmagyVA3RS2SPd47eKZ00GZZQcr+etTviHtc=
682+
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2/go.mod h1:yGhDiLKguA3iFJYxbrQkQiNzuy+ddxesSZYWVeeEH5Q=
681683
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
682684
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
683685
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA=

0 commit comments

Comments
 (0)