Skip to content

Commit

Permalink
Introducing Google Cloud KMS signing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bobveznat committed Dec 2, 2018
1 parent 719de56 commit 7952e33
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 19 deletions.
89 changes: 79 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
ssh-cert-authority
==================

.. image:: https://drone.io/github.com/cloudtools/ssh-cert-authority/status.png

Introduction
============

A democratic SSH certificate authority.

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

The idea here is that a user wishing to access a server runs
Expand Down Expand Up @@ -297,12 +295,18 @@ Effectively the format is::
sign complete requests. This should be the fingerprint of your CA. When using
this option you must, somehow, load the private key into the agent such that
the daemon can use it.
- ``PrivateKeyFile``: A path to a private key file. The key may be
unencrypted or have previously been encrypted using Amazon's KMS. If
the key was encrypted using KMS simply name it with a ".kms" extension
and ssh-cert-authority will attempt to decrypt the key on startup. See
the section on Encrypting a CA Key for help in using KMS to encrypt
the key.
- ``PrivateKeyFile``: A path to a private key file or a Google KMS key url.

If you have specified a file system path the key may be unencrypted or have
previousl been encrypted using Amazon's KMS. If the key was encrypted using
KMS simply name it with a ".kms" extension and ssh-cert-authority will
attempt to decrypt the key on startup. See the section on Encrypting a CA Key
for help in using KMS to encrypt the key.

If you specified a Google KMS key it should be of the form:
``gcpkms:///projects/<project-name>/locations/<region|global>/keyRings/<keyring
name>/cryptoKeys/<keyname>/cryptoKeyVersions/<version-number>``

- ``KmsRegion``: If sign_certd encounters a privatekey file with an
extension of ".kms" it will attempt to decrypt it using KMS in the
same region that the software is running in. It determines this using
Expand Down Expand Up @@ -385,6 +389,71 @@ Command Line Flags
permits a malicious user to control the IP address that is written to
log files.

Storing Your CA Signing Key in Google Cloud
===========================================
Google Cloud KMS supports signing operations and ssh-cert-authority can use
these keys to sign the SSH certificates it issues. If you do this you'll likely
want to have your ssh-cert-authority running on an instance in GCP and
configured with a service account that can use the key.

ssh-cert-authority has been tested with ecdsa keys from prime256v1 both
software and hardware backed. Other key kinds and curves might work.

This example assumes you have a functioning gcloud already.

Setting up keys::

# First create a keyring to store keys
gcloud kms keyrings create ssh-cert-authority-demo --location us-central1

# Create keys on that keyring for dev and prod
gcloud alpha kms keys create --purpose asymmetric-signing --keyring ssh-cert-authority-demo \
--location us-central1 --default-algorithm ec-sign-p256-sha256 dev
gcloud alpha kms keys create --purpose asymmetric-signing --keyring ssh-cert-authority-demo \
--location us-central1 --default-algorithm ec-sign-p256-sha256 prod

# Create a service account for the system
gcloud iam service-accounts create ssh-cert-authority-demo

# If you're using a GCP instance you should launch your instance and specify
# that service account as the account for the instance. If you're running
# this on a local machine or an AWS instance or something you will need to
# explicitly get the service account key
gcloud iam service-accounts keys create ssh-cert-authority-demo-serviceaccount.json
--iam-account ssh-cert-authority-demo@YOUR_GOOGLE_PROJECT_ID.iam.gserviceaccount.com
# You need to set that key file in an environment variable now:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/ssh-cert-authority-demo-serviceaccount.json

# Give that service account permission to use our newly created keys:
gcloud kms keys add-iam-policy-binding ssh-cert-authority-dev-hsm --location us-central1 \
--keyring ssh-cert-authority-demo \
--member serviceAccount:ssh-cert-authority-demo@YOUR_GOOGLE_PROJECT_ID.iam.gserviceaccount.com \
--role roles/cloudkms.signerVerifier

# Get the path to the keys we created:
gcloud kms keys list --location us-central1 --keyring ssh-cert-authority-demo

# That will print out the two keys we created earlier including the name of
# the key. The name of the key is a big path that begins with projects/. We
# need to copy this entire path into our sign_certd_config.json as the
# PrivateKeyFile for the environment. A minimal example showing only dev:
{
"dev": {
"NumberSignersRequired": -1,
"MaxCertLifetime": 86400,
"PrivateKeyFile": "gcpkms:///projects/YOUR_GOOGLE_PROJECT_ID/locations/us-central1/keyRings/ssh-cert-authority-demo/cryptoKeys/dev/cryptoKeyVersions/1",
"AuthorizedSigners": {
"a7:64:9e:35:5d:ae:c6:bd:79:f1:e3:c8:92:0b:9a:51": "bvz"
},
"AuthorizedUsers": {
"a7:64:9e:35:5d:ae:c6:bd:79:f1:e3:c8:92:0b:9a:51": "bvz"
}
}
}


Encrypting a CA Key Using Amazon's KMS
======================================

Expand Down
22 changes: 22 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module github.com/cloudtools/ssh-cert-authority

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

func (h *certRequestHandler) setupPrivateKeys(config map[string]ssh_ca_util.SignerdConfig) error {
for env, cfg := range config {
if cfg.PrivateKeyFile != "" {
keyContents, err := ioutil.ReadFile(cfg.PrivateKeyFile)
if cfg.PrivateKeyFile == "" {
continue
}
keyUrl, err := url.Parse(cfg.PrivateKeyFile)
if err != nil {
log.Printf("Ignoring invalid private key file: '%s'. Error parsing: %s", cfg.PrivateKeyFile, err)
continue
}
if keyUrl.Scheme == "gcpkms" {
cfg = config[env]
cfg.SigningKeyFingerprint = cfg.PrivateKeyFile
config[env] = cfg
} else if keyUrl.Scheme == "" || keyUrl.Scheme == "file" {
keyContents, err := ioutil.ReadFile(keyUrl.Path)
if err != nil {
return fmt.Errorf("Failed reading private key file %s: %v", cfg.PrivateKeyFile, err)
return fmt.Errorf("Failed reading private key file %s: %v", keyUrl.Path, err)
}
if strings.HasSuffix(cfg.PrivateKeyFile, ".kms") {
if strings.HasSuffix(keyUrl.Path, ".kms") {
var region string
if cfg.KmsRegion != "" {
region = cfg.KmsRegion
Expand All @@ -163,21 +176,21 @@ func (h *certRequestHandler) setupPrivateKeys(config map[string]ssh_ca_util.Sign
}
key, err := ssh.ParseRawPrivateKey(keyContents)
if err != nil {
return fmt.Errorf("Failed parsing private key %s: %v", cfg.PrivateKeyFile, err)
return fmt.Errorf("Failed parsing private key %s: %v", keyUrl.Path, err)
}
keyToAdd := agent.AddedKey{
PrivateKey: key,
Comment: fmt.Sprintf("ssh-cert-authority-%s-%s", env, cfg.PrivateKeyFile),
Comment: fmt.Sprintf("ssh-cert-authority-%s-%s", env, keyUrl.Path),
LifetimeSecs: 0,
}
agentClient := agent.NewClient(h.sshAgentConn)
err = agentClient.Add(keyToAdd)
if err != nil {
return fmt.Errorf("Unable to add private key %s: %v", cfg.PrivateKeyFile, err)
return fmt.Errorf("Unable to add private key %s: %v", keyUrl.Path, err)
}
signer, err := ssh.NewSignerFromKey(key)
if err != nil {
return fmt.Errorf("Unable to create signer from pk %s: %v", cfg.PrivateKeyFile, err)
return fmt.Errorf("Unable to create signer from pk %s: %v", keyUrl.Path, err)
}
keyFp := ssh_ca_util.MakeFingerprint(signer.PublicKey().Marshal())
log.Printf("Added private key for env %s: %s", env, keyFp)
Expand Down Expand Up @@ -644,7 +657,7 @@ func (h *certRequestHandler) maybeSignWithCa(requestID string, numSignersRequire
log.Printf("Received %d signatures for %s, signing now.\n", len(h.state[requestID].signatures), requestID)
signer, err := ssh_ca_util.GetSignerForFingerprint(signingKeyFingerprint, h.sshAgentConn)
if err != nil {
log.Printf("Couldn't find signing key for request %s, unable to sign request\n", requestID)
log.Printf("Couldn't find signing key for request %s, unable to sign request: %s\n", requestID, err)
return false, fmt.Errorf("Couldn't find signing key, unable to sign. Sorry.")
}
stateInfo := h.state[requestID]
Expand Down
97 changes: 97 additions & 0 deletions signer/gcpkms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package signer

import (
"cloud.google.com/go/kms/apiv1"
"context"
"crypto"
"crypto/x509"
"encoding/pem"
"fmt"
"golang.org/x/crypto/ssh"
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
"io"
"strings"
"time"
)

// Singleton client
var kmsClient *kms.KeyManagementClient

type GcpKmsSigner struct {
keyUrl string
kmsClient *kms.KeyManagementClient
kmsPubKey crypto.PublicKey
}

func NewSshGcpKmsSigner(keyUrl string) (ssh.Signer, error) {
kmsSigner, err := NewGcpKmsSigner(keyUrl)
if err != nil {
return nil, err
}
return ssh.NewSignerFromSigner(kmsSigner)
}

func NewGcpKmsSigner(keyUrl string) (*GcpKmsSigner, error) {
keyUrl = strings.TrimPrefix(keyUrl, "/")
kmsClient, err := getKmsClient()
if err != nil {
return nil, fmt.Errorf("Unable to initialize kms client: %s", err)
}
ctx := context.Background()
ctx, _ = context.WithTimeout(ctx, 10*time.Second)
getPubKeyReq := &kmspb.GetPublicKeyRequest{
Name: keyUrl,
}
kmsPubKeypb, err := kmsClient.GetPublicKey(ctx, getPubKeyReq)
if err != nil {
return nil, fmt.Errorf("Unable to get signing public key from kms: %s", err)
}
block, _ := pem.Decode([]byte(kmsPubKeypb.Pem))
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("Unable to parse kms public key: %s", err)
}

kmsSigner := &GcpKmsSigner{
keyUrl: keyUrl,
kmsClient: kmsClient,
kmsPubKey: pubKey,
}
return kmsSigner, nil
}

// PublicKey returns an associated PublicKey instance.
func (g GcpKmsSigner) Public() crypto.PublicKey {
return g.kmsPubKey
}

func (g GcpKmsSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
ctx := context.Background()
ctx, _ = context.WithTimeout(ctx, 10*time.Second)

req := &kmspb.AsymmetricSignRequest{
Name: g.keyUrl,
Digest: &kmspb.Digest{
Digest: &kmspb.Digest_Sha256{
Sha256: digest,
},
},
}
resp, err := g.kmsClient.AsymmetricSign(ctx, req)
if err != nil {
return nil, fmt.Errorf("Unable to sign: %s", err)
}
return resp.GetSignature(), nil
}

func getKmsClient() (*kms.KeyManagementClient, error) {
if kmsClient != nil {
return kmsClient, nil
}
ctx := context.Background()
c, err := kms.NewKeyManagementClient(ctx)
if err != nil {
return nil, err
}
return c, nil
}
Loading

0 comments on commit 7952e33

Please sign in to comment.