Skip to content

Commit 361891d

Browse files
committed
Support GCP KMS for checkpoint signer
1 parent 4fb2c2e commit 361891d

File tree

9 files changed

+180
-9
lines changed

9 files changed

+180
-9
lines changed

cmd/gcp/kms.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2024 Google LLC. 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/x509"
21+
"encoding/pem"
22+
"errors"
23+
"fmt"
24+
"io"
25+
26+
kms "cloud.google.com/go/kms/apiv1"
27+
"cloud.google.com/go/kms/apiv1/kmspb"
28+
)
29+
30+
// Signer is a GCP KMS implementation of
31+
// [crypto signer](https://pkg.go.dev/crypto#Signer).
32+
type Signer struct {
33+
// ctx must be stored because Signer is used as an implementation of the
34+
// crypto.Signer interface, which does not allow for a context in the Sign
35+
// method. However, the KMS AsymmetricSign API requires a context.
36+
ctx context.Context
37+
client *kms.KeyManagementClient
38+
keyName string
39+
publicKey crypto.PublicKey
40+
}
41+
42+
// Public returns the public key stored in the Signer object.
43+
func (s *Signer) Public() crypto.PublicKey {
44+
return s.publicKey
45+
}
46+
47+
// Sign signs the digest using the KMS signing key remotely on GCP.
48+
// Only crypto.SHA256 is supported.
49+
func (s *Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
50+
// Verify hash function and digest bytes length.
51+
if opts == nil || 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+
// Build the signing request and call the remote signing.
59+
req := &kmspb.AsymmetricSignRequest{
60+
Name: s.keyName,
61+
Digest: &kmspb.Digest{
62+
Digest: &kmspb.Digest_Sha256{
63+
Sha256: digest,
64+
},
65+
},
66+
}
67+
resp, err := s.client.AsymmetricSign(s.ctx, req)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to sign data: %w", err)
70+
}
71+
72+
// Perform integrity verification on result.
73+
if resp.Name != s.keyName {
74+
return nil, fmt.Errorf("request corrupted in-transit: %w", err)
75+
}
76+
77+
return resp.GetSignature(), nil
78+
}
79+
80+
// NewKMSSigner creates a new signer that uses GCP KMS to sign digests.
81+
func NewKMSSigner(ctx context.Context, keyName string) (*Signer, error) {
82+
kmClient, err := kms.NewKeyManagementClient(ctx)
83+
if err != nil {
84+
return nil, fmt.Errorf("failed to create KeyManagementClient: %w", err)
85+
}
86+
87+
// Retrieve the public key from GCP KMS
88+
req := &kmspb.GetPublicKeyRequest{
89+
Name: keyName,
90+
}
91+
resp, err := kmClient.GetPublicKey(ctx, req)
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
pemBlock, rest := pem.Decode([]byte(resp.Pem))
97+
if pemBlock == nil {
98+
return nil, errors.New("failed to decode PEM")
99+
}
100+
if len(rest) > 0 {
101+
return nil, fmt.Errorf("extra data after decoding PEM: %v", rest)
102+
}
103+
104+
var publicKey crypto.PublicKey
105+
switch pemBlock.Type {
106+
case "PUBLIC KEY":
107+
publicKey, err = x509.ParsePKIXPublicKey(pemBlock.Bytes)
108+
case "RSA PUBLIC KEY":
109+
publicKey, err = x509.ParsePKCS1PublicKey(pemBlock.Bytes)
110+
default:
111+
return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type)
112+
}
113+
if err != nil {
114+
return nil, err
115+
}
116+
117+
return &Signer{
118+
ctx: ctx,
119+
client: kmClient,
120+
keyName: keyName,
121+
publicKey: publicKey,
122+
}, nil
123+
}

cmd/gcp/main.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727
"syscall"
2828
"time"
2929

30-
"github.com/google/trillian/crypto/keys/pem"
3130
"github.com/google/trillian/monitoring/opencensus"
3231
"github.com/google/trillian/monitoring/prometheus"
3332
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -67,8 +66,7 @@ var (
6766
rejectUnexpired = flag.Bool("reject_unexpired", false, "If true then CTFE rejects certificates that are either currently valid or not yet valid.")
6867
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.")
6968
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.")
70-
privKey = flag.String("private_key", "", "Path to a private key .der file. Used to sign checkpoints and SCTs.")
71-
privKeyPass = flag.String("password", "", "private_key password.")
69+
kmsKeyName = flag.String("kms_key", "", "GCP KMS key name for signing checkpoints and SCTs. Format: projects/{projectId}/locations/{kmsRegion}/keyRings/{kmsKeyRing}/cryptoKeys/{keyName}/cryptoKeyVersions/{keyVersion}.")
7270
)
7371

7472
// nolint:staticcheck
@@ -77,10 +75,9 @@ func main() {
7775
flag.Parse()
7876
ctx := context.Background()
7977

80-
// TODO(phboneff): move to something else, like KMS
81-
signer, err := pem.ReadPrivateKeyFile(*privKey, *privKeyPass)
78+
signer, err := NewKMSSigner(ctx, *kmsKeyName)
8279
if err != nil {
83-
klog.Exitf("Can't open key: %v", err)
80+
klog.Exitf("Can't create KMS signer: %v", err)
8481
}
8582

8683
vCfg, err := sctfe.ValidateLogConfig(*origin, *projectID, *bucket, *spannerDB, *rootsPemFile, *rejectExpired, *rejectUnexpired, *extKeyUsages, *rejectExtensions, notAfterStart.t, notAfterLimit.t, signer)

config.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package sctfe
1616

1717
import (
1818
"crypto"
19+
"crypto/ecdsa"
20+
"crypto/rsa"
1921
"errors"
2022
"fmt"
2123
"strings"
@@ -32,7 +34,6 @@ type ValidatedLogConfig struct {
3234
// is also its submission prefix, as per https://c2sp.org/static-ct-api.
3335
Origin string
3436
// Used to sign the checkpoint and SCTs.
35-
// TODO(phboneff): check that this is RSA or ECDSA only.
3637
Signer crypto.Signer
3738
// If set, ExtKeyUsages will restrict the set of such usages that the
3839
// server will accept. By default all are accepted. The values specified
@@ -90,6 +91,16 @@ func ValidateLogConfig(origin string, projectID string, bucket string, spannerDB
9091
return nil, errors.New("empty rootsPemFile")
9192
}
9293

94+
// Validate signer that only RSA or ECDSA are supported.
95+
if signer == nil {
96+
return nil, errors.New("empty signer")
97+
}
98+
switch keyType := signer.Public().(type) {
99+
case *rsa.PublicKey, *ecdsa.PublicKey:
100+
default:
101+
return nil, fmt.Errorf("unsupported key type: %v", keyType)
102+
}
103+
93104
lExtKeyUsages := []string{}
94105
lRejectExtensions := []string{}
95106
if extKeyUsages != "" {

deployment/live/gcp/test/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ Terraforming the project can be done by:
3939
2. Run `terragrunt apply`
4040

4141
## Run the SCTFE
42+
4243
### With fake chains
4344

4445
On the VM, run the following command to bring up the SCTFE:
4546

4647
```bash
47-
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 --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}
48+
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 --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} --kms_key=projects/${GOOGLE_PROJECT}/locations/global/keyRings/${TESSERA_BASE_NAME}/cryptoKeys/sctfe-p256-sha256/cryptoKeyVersions/1
4849
```
4950

5051
In a different terminal you can either mint and submit certificates manually, or
@@ -116,7 +117,7 @@ Run the SCTFE with the same roots:
116117

117118
```bash
118119
cd ${SCTFE_REPO}
119-
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 -v=3
120+
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 --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 --kms_key=projects/${GOOGLE_PROJECT}/locations/global/keyRings/${TESSERA_BASE_NAME}/cryptoKeys/sctfe-p256-sha256/cryptoKeyVersions/1 -v=3
120121
```
121122

122123
Run `ct_hammer` in a different terminal:

deployment/modules/gcp/storage/main.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,27 @@ resource "google_spanner_database" "dedup_db" {
6565
"CREATE TABLE IDSeq (id INT64 NOT NULL, h BYTES(MAX) NOT NULL, idx INT64 NOT NULL,) PRIMARY KEY (id, h)",
6666
]
6767
}
68+
69+
# KMS
70+
71+
resource "google_kms_key_ring" "sctfe-keyring" {
72+
name = var.base_name
73+
location = var.kms_location
74+
}
75+
76+
resource "google_kms_crypto_key" "sctfe-asymmetric-sign-key-p256-sha256" {
77+
name = "sctfe-p256-sha256"
78+
key_ring = google_kms_key_ring.sctfe-keyring.id
79+
purpose = "ASYMMETRIC_SIGN"
80+
81+
version_template {
82+
algorithm = "EC_SIGN_P256_SHA256"
83+
protection_level = "SOFTWARE"
84+
}
85+
86+
lifecycle {
87+
prevent_destroy = true
88+
}
89+
90+
depends_on = [google_kms_key_ring.sctfe-keyring]
91+
}

deployment/modules/gcp/storage/outputs.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ output "dedup_spanner_db" {
1717
description = "Dedup Spanner database"
1818
value = google_spanner_database.dedup_db
1919
}
20+
21+
output "kms_key" {
22+
description = "KMS asymmetric sign key (P256_SHA256)"
23+
value = google_kms_crypto_key.sctfe-asymmetric-sign-key-p256-sha256
24+
}

deployment/modules/gcp/storage/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@ variable "location" {
1212
description = "Location in which to create resources"
1313
type = string
1414
}
15+
16+
variable "kms_location" {
17+
type = string
18+
description = "Location of KMS keyring"
19+
default = "global"
20+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/transparency-dev/static-ct
33
go 1.23.1
44

55
require (
6+
cloud.google.com/go/kms v1.20.0
67
cloud.google.com/go/spanner v1.71.0
78
cloud.google.com/go/storage v1.46.0
89
github.com/golang/mock v1.6.0
@@ -30,6 +31,7 @@ require (
3031
cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect
3132
cloud.google.com/go/compute/metadata v0.5.2 // indirect
3233
cloud.google.com/go/iam v1.2.1 // indirect
34+
cloud.google.com/go/longrunning v0.6.1 // indirect
3335
cloud.google.com/go/monitoring v1.21.1 // indirect
3436
cloud.google.com/go/trace v1.11.1 // indirect
3537
contrib.go.opencensus.io/exporter/stackdriver v0.13.14 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4
340340
cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w=
341341
cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24=
342342
cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI=
343+
cloud.google.com/go/kms v1.20.0 h1:uKUvjGqbBlI96xGE669hcVnEMw1Px/Mvfa62dhM5UrY=
344+
cloud.google.com/go/kms v1.20.0/go.mod h1:/dMbFF1tLLFnQV44AoI2GlotbjowyUfgVwezxW291fM=
343345
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
344346
cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
345347
cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE=

0 commit comments

Comments
 (0)