Skip to content

Commit 7952e33

Browse files
committed
Introducing Google Cloud KMS signing
If you're a google cloud user you can root your CA in one of their keys instead of mucking around with keys in ssh-agent. I also ported us to go modules with this change.
1 parent 719de56 commit 7952e33

File tree

6 files changed

+315
-19
lines changed

6 files changed

+315
-19
lines changed

README.rst

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22
ssh-cert-authority
33
==================
44

5-
.. image:: https://drone.io/github.com/cloudtools/ssh-cert-authority/status.png
6-
75
Introduction
86
============
97

108
A democratic SSH certificate authority.
119

1210
Operators of ssh-cert-authority want to use SSH certificates to provide
13-
fine-grained access control to servers they operate, keep their
14-
certificate signing key a secret and not need to be required to get
11+
fine-grained access control to servers they operate, enforce the 2-person rule,
12+
keep their certificate signing key a secret and not need to be required to get
1513
involved to actually sign certificates. A tall order.
1614

1715
The idea here is that a user wishing to access a server runs
@@ -297,12 +295,18 @@ Effectively the format is::
297295
sign complete requests. This should be the fingerprint of your CA. When using
298296
this option you must, somehow, load the private key into the agent such that
299297
the daemon can use it.
300-
- ``PrivateKeyFile``: A path to a private key file. The key may be
301-
unencrypted or have previously been encrypted using Amazon's KMS. If
302-
the key was encrypted using KMS simply name it with a ".kms" extension
303-
and ssh-cert-authority will attempt to decrypt the key on startup. See
304-
the section on Encrypting a CA Key for help in using KMS to encrypt
305-
the key.
298+
- ``PrivateKeyFile``: A path to a private key file or a Google KMS key url.
299+
300+
If you have specified a file system path the key may be unencrypted or have
301+
previousl been encrypted using Amazon's KMS. If the key was encrypted using
302+
KMS simply name it with a ".kms" extension and ssh-cert-authority will
303+
attempt to decrypt the key on startup. See the section on Encrypting a CA Key
304+
for help in using KMS to encrypt the key.
305+
306+
If you specified a Google KMS key it should be of the form:
307+
``gcpkms:///projects/<project-name>/locations/<region|global>/keyRings/<keyring
308+
name>/cryptoKeys/<keyname>/cryptoKeyVersions/<version-number>``
309+
306310
- ``KmsRegion``: If sign_certd encounters a privatekey file with an
307311
extension of ".kms" it will attempt to decrypt it using KMS in the
308312
same region that the software is running in. It determines this using
@@ -385,6 +389,71 @@ Command Line Flags
385389
permits a malicious user to control the IP address that is written to
386390
log files.
387391

392+
Storing Your CA Signing Key in Google Cloud
393+
===========================================
394+
Google Cloud KMS supports signing operations and ssh-cert-authority can use
395+
these keys to sign the SSH certificates it issues. If you do this you'll likely
396+
want to have your ssh-cert-authority running on an instance in GCP and
397+
configured with a service account that can use the key.
398+
399+
ssh-cert-authority has been tested with ecdsa keys from prime256v1 both
400+
software and hardware backed. Other key kinds and curves might work.
401+
402+
This example assumes you have a functioning gcloud already.
403+
404+
Setting up keys::
405+
406+
# First create a keyring to store keys
407+
gcloud kms keyrings create ssh-cert-authority-demo --location us-central1
408+
409+
# Create keys on that keyring for dev and prod
410+
gcloud alpha kms keys create --purpose asymmetric-signing --keyring ssh-cert-authority-demo \
411+
--location us-central1 --default-algorithm ec-sign-p256-sha256 dev
412+
gcloud alpha kms keys create --purpose asymmetric-signing --keyring ssh-cert-authority-demo \
413+
--location us-central1 --default-algorithm ec-sign-p256-sha256 prod
414+
415+
# Create a service account for the system
416+
gcloud iam service-accounts create ssh-cert-authority-demo
417+
418+
# If you're using a GCP instance you should launch your instance and specify
419+
# that service account as the account for the instance. If you're running
420+
# this on a local machine or an AWS instance or something you will need to
421+
# explicitly get the service account key
422+
gcloud iam service-accounts keys create ssh-cert-authority-demo-serviceaccount.json
423+
--iam-account ssh-cert-authority-demo@YOUR_GOOGLE_PROJECT_ID.iam.gserviceaccount.com
424+
425+
# You need to set that key file in an environment variable now:
426+
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/ssh-cert-authority-demo-serviceaccount.json
427+
428+
# Give that service account permission to use our newly created keys:
429+
gcloud kms keys add-iam-policy-binding ssh-cert-authority-dev-hsm --location us-central1 \
430+
--keyring ssh-cert-authority-demo \
431+
--member serviceAccount:ssh-cert-authority-demo@YOUR_GOOGLE_PROJECT_ID.iam.gserviceaccount.com \
432+
--role roles/cloudkms.signerVerifier
433+
434+
# Get the path to the keys we created:
435+
gcloud kms keys list --location us-central1 --keyring ssh-cert-authority-demo
436+
437+
# That will print out the two keys we created earlier including the name of
438+
# the key. The name of the key is a big path that begins with projects/. We
439+
# need to copy this entire path into our sign_certd_config.json as the
440+
# PrivateKeyFile for the environment. A minimal example showing only dev:
441+
442+
{
443+
"dev": {
444+
"NumberSignersRequired": -1,
445+
"MaxCertLifetime": 86400,
446+
"PrivateKeyFile": "gcpkms:///projects/YOUR_GOOGLE_PROJECT_ID/locations/us-central1/keyRings/ssh-cert-authority-demo/cryptoKeys/dev/cryptoKeyVersions/1",
447+
"AuthorizedSigners": {
448+
"a7:64:9e:35:5d:ae:c6:bd:79:f1:e3:c8:92:0b:9a:51": "bvz"
449+
},
450+
"AuthorizedUsers": {
451+
"a7:64:9e:35:5d:ae:c6:bd:79:f1:e3:c8:92:0b:9a:51": "bvz"
452+
}
453+
}
454+
}
455+
456+
388457
Encrypting a CA Key Using Amazon's KMS
389458
======================================
390459

go.mod

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module github.com/cloudtools/ssh-cert-authority
2+
3+
require (
4+
cloud.google.com/go v0.33.0
5+
cloud.google.com/go/compute/metadata v0.0.0-20181115181204-d50f0e9b2506 // indirect
6+
github.com/aws/aws-sdk-go v1.15.76
7+
github.com/codegangsta/cli v1.20.0
8+
github.com/davecgh/go-spew v1.1.1 // indirect
9+
github.com/googleapis/gax-go v2.0.2+incompatible // indirect
10+
github.com/gorilla/context v1.1.1 // indirect
11+
github.com/gorilla/handlers v1.4.0
12+
github.com/gorilla/mux v1.6.2
13+
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
github.com/stretchr/testify v1.2.2 // indirect
15+
go.opencensus.io v0.18.0 // indirect
16+
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869
17+
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect
18+
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288
19+
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 // indirect
20+
google.golang.org/api v0.0.0-20181114235557-83a9d304b1e6
21+
google.golang.org/genproto v0.0.0-20181109154231-b5d43981345b
22+
)

go.sum

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2+
cloud.google.com/go v0.33.0 h1:1kNZapR5iXMPsPEca6Rqg+EN4/8/ZukNjMdwNQEllWk=
3+
cloud.google.com/go v0.33.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
4+
cloud.google.com/go/compute/metadata v0.0.0-20181115181204-d50f0e9b2506 h1:toHF+GJCU8Zr/qhrb6FOELllmvo6e+Np7FdhZFX9SHA=
5+
cloud.google.com/go/compute/metadata v0.0.0-20181115181204-d50f0e9b2506/go.mod h1:bDzgiyYlSneEi8ypjdQR5QS9yAMUX2nlrSb6UVd6Ghk=
6+
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
7+
github.com/aws/aws-sdk-go v1.15.76 h1:AZB4clNWIk13YJaTm07kqyrHkj7gZYBQCgyTh/v4Sec=
8+
github.com/aws/aws-sdk-go v1.15.76/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
9+
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
10+
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
11+
github.com/codegangsta/cli v1.20.0 h1:iX1FXEgwzd5+XN6wk5cVHOGQj6Q3Dcp20lUeS4lHNTw=
12+
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
13+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
14+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15+
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
16+
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
17+
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
18+
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
19+
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
20+
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
21+
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
22+
github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww=
23+
github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
24+
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
25+
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
26+
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
27+
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
28+
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
29+
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
30+
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
31+
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
32+
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
33+
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
34+
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
35+
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
36+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
37+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
38+
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
39+
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
40+
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
41+
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
42+
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
43+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
44+
go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938=
45+
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
46+
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
47+
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
48+
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
49+
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
50+
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
51+
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
52+
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
53+
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
54+
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
55+
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0=
56+
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
57+
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
58+
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
59+
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
60+
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
61+
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
62+
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
63+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
64+
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
65+
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
66+
google.golang.org/api v0.0.0-20181114235557-83a9d304b1e6 h1:oDEtqBIUq5MDzbdy1TgCnw2sW+63bnr1N1OoBZWhLOc=
67+
google.golang.org/api v0.0.0-20181114235557-83a9d304b1e6/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
68+
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
69+
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
70+
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
71+
google.golang.org/genproto v0.0.0-20181109154231-b5d43981345b h1:WkFtVmaZoTRVoRYr0LTC9SYNhlw0X0HrVPz2OVssVm4=
72+
google.golang.org/genproto v0.0.0-20181109154231-b5d43981345b/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
73+
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
74+
google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY=
75+
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
76+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
77+
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
78+
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

sign_certd.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"log"
2626
"net"
2727
"net/http"
28+
"net/url"
2829
"os"
2930
"reflect"
3031
"regexp"
@@ -133,12 +134,24 @@ type signingRequest struct {
133134

134135
func (h *certRequestHandler) setupPrivateKeys(config map[string]ssh_ca_util.SignerdConfig) error {
135136
for env, cfg := range config {
136-
if cfg.PrivateKeyFile != "" {
137-
keyContents, err := ioutil.ReadFile(cfg.PrivateKeyFile)
137+
if cfg.PrivateKeyFile == "" {
138+
continue
139+
}
140+
keyUrl, err := url.Parse(cfg.PrivateKeyFile)
141+
if err != nil {
142+
log.Printf("Ignoring invalid private key file: '%s'. Error parsing: %s", cfg.PrivateKeyFile, err)
143+
continue
144+
}
145+
if keyUrl.Scheme == "gcpkms" {
146+
cfg = config[env]
147+
cfg.SigningKeyFingerprint = cfg.PrivateKeyFile
148+
config[env] = cfg
149+
} else if keyUrl.Scheme == "" || keyUrl.Scheme == "file" {
150+
keyContents, err := ioutil.ReadFile(keyUrl.Path)
138151
if err != nil {
139-
return fmt.Errorf("Failed reading private key file %s: %v", cfg.PrivateKeyFile, err)
152+
return fmt.Errorf("Failed reading private key file %s: %v", keyUrl.Path, err)
140153
}
141-
if strings.HasSuffix(cfg.PrivateKeyFile, ".kms") {
154+
if strings.HasSuffix(keyUrl.Path, ".kms") {
142155
var region string
143156
if cfg.KmsRegion != "" {
144157
region = cfg.KmsRegion
@@ -163,21 +176,21 @@ func (h *certRequestHandler) setupPrivateKeys(config map[string]ssh_ca_util.Sign
163176
}
164177
key, err := ssh.ParseRawPrivateKey(keyContents)
165178
if err != nil {
166-
return fmt.Errorf("Failed parsing private key %s: %v", cfg.PrivateKeyFile, err)
179+
return fmt.Errorf("Failed parsing private key %s: %v", keyUrl.Path, err)
167180
}
168181
keyToAdd := agent.AddedKey{
169182
PrivateKey: key,
170-
Comment: fmt.Sprintf("ssh-cert-authority-%s-%s", env, cfg.PrivateKeyFile),
183+
Comment: fmt.Sprintf("ssh-cert-authority-%s-%s", env, keyUrl.Path),
171184
LifetimeSecs: 0,
172185
}
173186
agentClient := agent.NewClient(h.sshAgentConn)
174187
err = agentClient.Add(keyToAdd)
175188
if err != nil {
176-
return fmt.Errorf("Unable to add private key %s: %v", cfg.PrivateKeyFile, err)
189+
return fmt.Errorf("Unable to add private key %s: %v", keyUrl.Path, err)
177190
}
178191
signer, err := ssh.NewSignerFromKey(key)
179192
if err != nil {
180-
return fmt.Errorf("Unable to create signer from pk %s: %v", cfg.PrivateKeyFile, err)
193+
return fmt.Errorf("Unable to create signer from pk %s: %v", keyUrl.Path, err)
181194
}
182195
keyFp := ssh_ca_util.MakeFingerprint(signer.PublicKey().Marshal())
183196
log.Printf("Added private key for env %s: %s", env, keyFp)
@@ -644,7 +657,7 @@ func (h *certRequestHandler) maybeSignWithCa(requestID string, numSignersRequire
644657
log.Printf("Received %d signatures for %s, signing now.\n", len(h.state[requestID].signatures), requestID)
645658
signer, err := ssh_ca_util.GetSignerForFingerprint(signingKeyFingerprint, h.sshAgentConn)
646659
if err != nil {
647-
log.Printf("Couldn't find signing key for request %s, unable to sign request\n", requestID)
660+
log.Printf("Couldn't find signing key for request %s, unable to sign request: %s\n", requestID, err)
648661
return false, fmt.Errorf("Couldn't find signing key, unable to sign. Sorry.")
649662
}
650663
stateInfo := h.state[requestID]

signer/gcpkms.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package signer
2+
3+
import (
4+
"cloud.google.com/go/kms/apiv1"
5+
"context"
6+
"crypto"
7+
"crypto/x509"
8+
"encoding/pem"
9+
"fmt"
10+
"golang.org/x/crypto/ssh"
11+
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
12+
"io"
13+
"strings"
14+
"time"
15+
)
16+
17+
// Singleton client
18+
var kmsClient *kms.KeyManagementClient
19+
20+
type GcpKmsSigner struct {
21+
keyUrl string
22+
kmsClient *kms.KeyManagementClient
23+
kmsPubKey crypto.PublicKey
24+
}
25+
26+
func NewSshGcpKmsSigner(keyUrl string) (ssh.Signer, error) {
27+
kmsSigner, err := NewGcpKmsSigner(keyUrl)
28+
if err != nil {
29+
return nil, err
30+
}
31+
return ssh.NewSignerFromSigner(kmsSigner)
32+
}
33+
34+
func NewGcpKmsSigner(keyUrl string) (*GcpKmsSigner, error) {
35+
keyUrl = strings.TrimPrefix(keyUrl, "/")
36+
kmsClient, err := getKmsClient()
37+
if err != nil {
38+
return nil, fmt.Errorf("Unable to initialize kms client: %s", err)
39+
}
40+
ctx := context.Background()
41+
ctx, _ = context.WithTimeout(ctx, 10*time.Second)
42+
getPubKeyReq := &kmspb.GetPublicKeyRequest{
43+
Name: keyUrl,
44+
}
45+
kmsPubKeypb, err := kmsClient.GetPublicKey(ctx, getPubKeyReq)
46+
if err != nil {
47+
return nil, fmt.Errorf("Unable to get signing public key from kms: %s", err)
48+
}
49+
block, _ := pem.Decode([]byte(kmsPubKeypb.Pem))
50+
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
51+
if err != nil {
52+
return nil, fmt.Errorf("Unable to parse kms public key: %s", err)
53+
}
54+
55+
kmsSigner := &GcpKmsSigner{
56+
keyUrl: keyUrl,
57+
kmsClient: kmsClient,
58+
kmsPubKey: pubKey,
59+
}
60+
return kmsSigner, nil
61+
}
62+
63+
// PublicKey returns an associated PublicKey instance.
64+
func (g GcpKmsSigner) Public() crypto.PublicKey {
65+
return g.kmsPubKey
66+
}
67+
68+
func (g GcpKmsSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
69+
ctx := context.Background()
70+
ctx, _ = context.WithTimeout(ctx, 10*time.Second)
71+
72+
req := &kmspb.AsymmetricSignRequest{
73+
Name: g.keyUrl,
74+
Digest: &kmspb.Digest{
75+
Digest: &kmspb.Digest_Sha256{
76+
Sha256: digest,
77+
},
78+
},
79+
}
80+
resp, err := g.kmsClient.AsymmetricSign(ctx, req)
81+
if err != nil {
82+
return nil, fmt.Errorf("Unable to sign: %s", err)
83+
}
84+
return resp.GetSignature(), nil
85+
}
86+
87+
func getKmsClient() (*kms.KeyManagementClient, error) {
88+
if kmsClient != nil {
89+
return kmsClient, nil
90+
}
91+
ctx := context.Background()
92+
c, err := kms.NewKeyManagementClient(ctx)
93+
if err != nil {
94+
return nil, err
95+
}
96+
return c, nil
97+
}

0 commit comments

Comments
 (0)