Skip to content

Commit 050af71

Browse files
committed
Add GitHub App token signer for GCP KMS.
1 parent f5f96dc commit 050af71

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ require (
66
chainguard.dev/sdk v0.1.19
77
cloud.google.com/go/bigquery v1.60.0
88
cloud.google.com/go/compute/metadata v0.2.3
9+
cloud.google.com/go/kms v1.15.7
910
cloud.google.com/go/pubsub v1.37.0
11+
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0
1012
github.com/chainguard-dev/clog v1.3.1
1113
github.com/cloudevents/sdk-go/v2 v2.15.2
14+
github.com/golang-jwt/jwt/v4 v4.5.0
1215
github.com/google/go-cmp v0.6.0
1316
github.com/google/go-github/v60 v60.0.0
1417
github.com/google/go-github/v61 v61.0.0

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
7272
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
7373
github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE=
7474
github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc=
75+
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 h1:XWuWBRFEpqVrHepQob9yPS3Xg4K3Wr9QCx4fu8HbUNg=
76+
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0/go.mod h1:qoGA4DxWPaYTgVCrmEspVSjlTu4WYAiSxMIhorMRXXc=
7577
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
7678
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
7779
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -106,6 +108,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
106108
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
107109
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
108110
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
111+
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
112+
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
109113
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
110114
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
111115
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=

pkg/github/kms/gcp.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright 2023 Chainguard, Inc.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package kms
7+
8+
import (
9+
"context"
10+
"encoding/base64"
11+
"errors"
12+
"fmt"
13+
14+
kms "cloud.google.com/go/kms/apiv1"
15+
"cloud.google.com/go/kms/apiv1/kmspb"
16+
"github.com/golang-jwt/jwt/v4"
17+
)
18+
19+
type SigningMethodGCP struct {
20+
ctx context.Context
21+
client *kms.KeyManagementClient
22+
}
23+
24+
func (s *SigningMethodGCP) Verify(string, string, interface{}) error {
25+
return errors.New("not implemented")
26+
}
27+
28+
func (s *SigningMethodGCP) Sign(signingString string, ikey interface{}) (string, error) {
29+
ctx := s.ctx
30+
31+
key, ok := ikey.(string)
32+
if !ok {
33+
return "", fmt.Errorf("invalid key reference type: %T", ikey)
34+
}
35+
req := &kmspb.AsymmetricSignRequest{
36+
Name: key,
37+
Data: []byte(signingString),
38+
}
39+
resp, err := s.client.AsymmetricSign(ctx, req)
40+
if err != nil {
41+
return "", err
42+
}
43+
return base64.RawURLEncoding.EncodeToString(resp.Signature), nil
44+
}
45+
46+
func (s *SigningMethodGCP) Alg() string {
47+
return "RS256"
48+
}
49+
50+
type GCPSigner struct {
51+
ctx context.Context
52+
client *kms.KeyManagementClient
53+
key string
54+
}
55+
56+
func NewGCP(ctx context.Context, client *kms.KeyManagementClient, key string) (*GCPSigner, error) {
57+
return &GCPSigner{
58+
ctx: ctx,
59+
client: client,
60+
key: key,
61+
}, nil
62+
}
63+
64+
// Sign signs the JWT claims with the RSA key.
65+
func (s *GCPSigner) Sign(claims jwt.Claims) (string, error) {
66+
method := &SigningMethodGCP{
67+
ctx: s.ctx,
68+
client: s.client,
69+
}
70+
return jwt.NewWithClaims(method, claims).SignedString(s.key)
71+
}

pkg/github/kms/gcp_test.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2023 Chainguard, Inc.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package kms
7+
8+
import (
9+
"context"
10+
"net"
11+
"testing"
12+
13+
kms "cloud.google.com/go/kms/apiv1"
14+
"cloud.google.com/go/kms/apiv1/kmspb"
15+
"github.com/golang-jwt/jwt/v4"
16+
"google.golang.org/api/option"
17+
"google.golang.org/grpc"
18+
"google.golang.org/grpc/credentials/insecure"
19+
)
20+
21+
type fakeGCPKMS struct {
22+
kmspb.UnimplementedKeyManagementServiceServer
23+
}
24+
25+
func (fakeGCPKMS) AsymmetricSign(context.Context, *kmspb.AsymmetricSignRequest) (*kmspb.AsymmetricSignResponse, error) {
26+
return &kmspb.AsymmetricSignResponse{
27+
Signature: []byte("fake"),
28+
}, nil
29+
}
30+
31+
func TestGCP(t *testing.T) {
32+
ctx := context.Background()
33+
34+
// Setup the fake server.
35+
impl := &fakeGCPKMS{}
36+
l, err := net.Listen("tcp", "localhost:0")
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
gsrv := grpc.NewServer()
41+
kmspb.RegisterKeyManagementServiceServer(gsrv, impl)
42+
fakeServerAddr := l.Addr().String()
43+
go func() {
44+
if err := gsrv.Serve(l); err != nil {
45+
panic(err)
46+
}
47+
}()
48+
49+
// Create a client.
50+
client, err := kms.NewKeyManagementClient(ctx,
51+
option.WithEndpoint(fakeServerAddr),
52+
option.WithoutAuthentication(),
53+
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
54+
)
55+
if err != nil {
56+
t.Fatal(err)
57+
}
58+
59+
signer, err := NewGCP(ctx, client, "foo")
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
64+
if _, err := signer.Sign(jwt.RegisteredClaims{
65+
Subject: "foo",
66+
Issuer: "bar",
67+
}); err != nil {
68+
t.Fatal(err)
69+
}
70+
}

pkg/github/kms/kms.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Copyright 2023 Chainguard, Inc.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package kms
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"os"
12+
"strings"
13+
14+
kms "cloud.google.com/go/kms/apiv1"
15+
"github.com/bradleyfalzon/ghinstallation/v2"
16+
"github.com/golang-jwt/jwt/v4"
17+
)
18+
19+
// NewSigner creates a new signer based on a key url.
20+
// Supported URL schemes:
21+
// - file://<path>: creates a signer from a PEM encoded RSA private key from a file.
22+
// - gcpkms://<key>: creates a remote signer for a GCP KMS key.
23+
func NewSigner(ctx context.Context, url string) (ghinstallation.Signer, error) {
24+
t := strings.SplitN(url, "://", 2)
25+
if len(t) < 2 {
26+
return nil, fmt.Errorf("invalid key format: %s", url)
27+
}
28+
29+
switch t[0] {
30+
case "file":
31+
pk, err := os.ReadFile(t[1])
32+
if err != nil {
33+
return nil, fmt.Errorf("could not open file: %w", err)
34+
}
35+
rsa, err := jwt.ParseRSAPrivateKeyFromPEM(pk)
36+
if err != nil {
37+
return nil, fmt.Errorf("could not parse private key: %w", err)
38+
}
39+
return ghinstallation.NewRSASigner(jwt.SigningMethodRS256, rsa), nil
40+
41+
case "gcpkms":
42+
client, err := kms.NewKeyManagementClient(ctx)
43+
if err != nil {
44+
return nil, fmt.Errorf("could not create kms client: %w", err)
45+
}
46+
return NewGCP(ctx, client, t[1])
47+
}
48+
return nil, fmt.Errorf("unknown key type: %s", t[0])
49+
}

0 commit comments

Comments
 (0)