Skip to content

Commit 35512b0

Browse files
committed
Support GCP KMS for checkpoint signer
1 parent 4fb2c2e commit 35512b0

File tree

8 files changed

+169
-9
lines changed

8 files changed

+169
-9
lines changed

cmd/gcp/kms.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 signer's public key.
43+
func (s *Signer) Public() crypto.PublicKey {
44+
return s.publicKey
45+
}
46+
47+
// Sign signs the data using the KMS signing key.
48+
func (s *Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
49+
// Verify hash function and digest bytes length.
50+
if opts == nil || opts.HashFunc() != crypto.SHA256 {
51+
return nil, fmt.Errorf("unsupported hash func: %v", opts.HashFunc())
52+
}
53+
if len(digest) != opts.HashFunc().Size() {
54+
return nil, fmt.Errorf("digest bytes length %d does not match hash function bytes length %d", len(digest), opts.HashFunc().Size())
55+
}
56+
57+
// Build the signing request and call the remote signing.
58+
req := &kmspb.AsymmetricSignRequest{
59+
Name: s.keyName,
60+
Digest: &kmspb.Digest{
61+
Digest: &kmspb.Digest_Sha256{
62+
Sha256: digest,
63+
},
64+
},
65+
}
66+
resp, err := s.client.AsymmetricSign(s.ctx, req)
67+
if err != nil {
68+
return nil, fmt.Errorf("failed to sign data: %w", err)
69+
}
70+
71+
// Perform integrity verification on result.
72+
if resp.Name != s.keyName {
73+
return nil, fmt.Errorf("request corrupted in-transit: %w", err)
74+
}
75+
76+
return resp.GetSignature(), nil
77+
}
78+
79+
// NewKMSSigner creates a new signer that uses GCP KMS to sign digests.
80+
func NewKMSSigner(ctx context.Context, keyName string) (*Signer, error) {
81+
kmClient, err := kms.NewKeyManagementClient(ctx)
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to create KeyManagementClient: %w", err)
84+
}
85+
86+
req := &kmspb.GetPublicKeyRequest{
87+
Name: keyName,
88+
}
89+
resp, err := kmClient.GetPublicKey(ctx, req)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
pemBlock, rest := pem.Decode([]byte(resp.Pem))
95+
if pemBlock == nil {
96+
return nil, errors.New("failed to decode PEM")
97+
}
98+
if len(rest) > 0 {
99+
return nil, fmt.Errorf("extra data after decoding PEM: %v", rest)
100+
}
101+
102+
var publicKey crypto.PublicKey
103+
switch pemBlock.Type {
104+
case "PUBLIC KEY":
105+
publicKey, err = x509.ParsePKIXPublicKey(pemBlock.Bytes)
106+
case "RSA PUBLIC KEY":
107+
publicKey, err = x509.ParsePKCS1PublicKey(pemBlock.Bytes)
108+
default:
109+
return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type)
110+
}
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
return &Signer{
116+
ctx: ctx,
117+
client: kmClient,
118+
keyName: keyName,
119+
publicKey: publicKey,
120+
}, nil
121+
}

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,24 @@ 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" "keyring" {
72+
name = var.base_name
73+
location = "global"
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.keyring.id
79+
purpose = "ASYMMETRIC_SIGN"
80+
81+
version_template {
82+
algorithm = "EC_SIGN_P256_SHA256"
83+
}
84+
85+
lifecycle {
86+
prevent_destroy = true
87+
}
88+
}

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+
}

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)