From f8c57c2f5770fc726a00ef48c2d8e8b63b5cff28 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Sun, 11 Mar 2018 13:20:08 -0400 Subject: [PATCH] Move ssh key functions to pkg/pki Preparation for reuse by the roundtrip mock tests. --- pkg/model/BUILD.bazel | 1 + pkg/model/names.go | 4 +- pkg/pki/BUILD.bazel | 8 +- pkg/pki/sshkey.go | 127 ++++++++++++++++++ .../awstasks => pkg/pki}/sshkey_test.go | 6 +- upup/pkg/fi/cloudup/awstasks/BUILD.bazel | 3 +- upup/pkg/fi/cloudup/awstasks/sshkey.go | 107 +-------------- 7 files changed, 145 insertions(+), 111 deletions(-) create mode 100644 pkg/pki/sshkey.go rename {upup/pkg/fi/cloudup/awstasks => pkg/pki}/sshkey_test.go (97%) diff --git a/pkg/model/BUILD.bazel b/pkg/model/BUILD.bazel index 0d2e6c075d..45a88224b8 100644 --- a/pkg/model/BUILD.bazel +++ b/pkg/model/BUILD.bazel @@ -30,6 +30,7 @@ go_library( "//pkg/model/components:go_default_library", "//pkg/model/iam:go_default_library", "//pkg/model/resources:go_default_library", + "//pkg/pki:go_default_library", "//pkg/tokens:go_default_library", "//upup/pkg/fi:go_default_library", "//upup/pkg/fi/cloudup/awstasks:go_default_library", diff --git a/pkg/model/names.go b/pkg/model/names.go index 6b90ec3d3b..ee6710740f 100644 --- a/pkg/model/names.go +++ b/pkg/model/names.go @@ -20,7 +20,9 @@ import ( "fmt" "github.com/golang/glog" + "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/pkg/pki" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" ) @@ -126,7 +128,7 @@ func (c *KopsModelContext) SSHKeyName() (string, error) { return name, nil } - fingerprint, err := awstasks.ComputeOpenSSHKeyFingerprint(string(c.SSHPublicKeys[0])) + fingerprint, err := pki.ComputeOpenSSHKeyFingerprint(string(c.SSHPublicKeys[0])) if err != nil { return "", err } diff --git a/pkg/pki/BUILD.bazel b/pkg/pki/BUILD.bazel index 85197bd1be..2642996439 100644 --- a/pkg/pki/BUILD.bazel +++ b/pkg/pki/BUILD.bazel @@ -6,10 +6,15 @@ go_library( "certificate.go", "csr.go", "privatekey.go", + "sshkey.go", ], importpath = "k8s.io/kops/pkg/pki", visibility = ["//visibility:public"], - deps = ["//vendor/github.com/golang/glog:go_default_library"], + deps = [ + "//upup/pkg/fi/utils:go_default_library", + "//vendor/github.com/golang/glog:go_default_library", + "//vendor/golang.org/x/crypto/ssh:go_default_library", + ], ) go_test( @@ -17,6 +22,7 @@ go_test( srcs = [ "certificate_test.go", "privatekey_test.go", + "sshkey_test.go", ], embed = [":go_default_library"], ) diff --git a/pkg/pki/sshkey.go b/pkg/pki/sshkey.go new file mode 100644 index 0000000000..0ebf7d0e33 --- /dev/null +++ b/pkg/pki/sshkey.go @@ -0,0 +1,127 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pki + +import ( + "bytes" + "crypto" + "crypto/md5" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "fmt" + "reflect" + "strings" + + "golang.org/x/crypto/ssh" + + "k8s.io/kops/upup/pkg/fi/utils" +) + +// parseSSHPublicKey parses the SSH public key string +func parseSSHPublicKey(publicKey string) (ssh.PublicKey, error) { + tokens := strings.Fields(publicKey) + if len(tokens) < 2 { + return nil, fmt.Errorf("error parsing SSH public key: %q", publicKey) + } + + sshPublicKeyBytes, err := base64.StdEncoding.DecodeString(tokens[1]) + if err != nil { + return nil, fmt.Errorf("error decoding SSH public key: %q err: %s", publicKey, err) + } + if len(tokens) < 2 { + return nil, fmt.Errorf("error decoding SSH public key: %q", publicKey) + } + + sshPublicKey, err := ssh.ParsePublicKey(sshPublicKeyBytes) + if err != nil { + return nil, fmt.Errorf("error parsing SSH public key: %v", err) + } + return sshPublicKey, nil +} + +// colonSeparatedHex formats the byte slice SSH-fingerprint style: hex bytes separated by colons +func colonSeparatedHex(data []byte) string { + sshKeyFingerprint := fmt.Sprintf("%x", data) + var colonSeparated bytes.Buffer + for i := 0; i < len(sshKeyFingerprint); i++ { + if (i%2) == 0 && i != 0 { + colonSeparated.WriteByte(':') + } + colonSeparated.WriteByte(sshKeyFingerprint[i]) + } + + return colonSeparated.String() +} + +// ComputeAWSKeyFingerprint computes the AWS-specific fingerprint of the SSH public key +func ComputeAWSKeyFingerprint(publicKey string) (string, error) { + sshPublicKey, err := parseSSHPublicKey(publicKey) + if err != nil { + return "", err + } + + der, err := toDER(sshPublicKey) + if err != nil { + return "", fmt.Errorf("error computing fingerprint for SSH public key: %v", err) + } + h := md5.Sum(der) + + return colonSeparatedHex(h[:]), nil +} + +// ComputeOpenSSHKeyFingerprint computes the OpenSSH fingerprint of the SSH public key +func ComputeOpenSSHKeyFingerprint(publicKey string) (string, error) { + sshPublicKey, err := parseSSHPublicKey(publicKey) + if err != nil { + return "", err + } + + h := md5.Sum(sshPublicKey.Marshal()) + return colonSeparatedHex(h[:]), nil +} + +// toDER gets the DER encoding of the SSH public key +// Annoyingly, the ssh code wraps the actual crypto keys, so we have to use reflection tricks +func toDER(pubkey ssh.PublicKey) ([]byte, error) { + pubkeyValue := reflect.ValueOf(pubkey) + typeName := utils.BuildTypeName(pubkeyValue.Type()) + + var cryptoKey crypto.PublicKey + switch typeName { + case "*rsaPublicKey": + var rsaPublicKey *rsa.PublicKey + targetType := reflect.ValueOf(rsaPublicKey).Type() + rsaPublicKey = pubkeyValue.Convert(targetType).Interface().(*rsa.PublicKey) + cryptoKey = rsaPublicKey + + //case "*dsaPublicKey": + // var dsaPublicKey *dsa.PublicKey + // targetType := reflect.ValueOf(dsaPublicKey).Type() + // dsaPublicKey = pubkeyValue.Convert(targetType).Interface().(*dsa.PublicKey) + // cryptoKey = dsaPublicKey + + default: + return nil, fmt.Errorf("Unexpected type of SSH key (%q); AWS can only import RSA keys", typeName) + } + + der, err := x509.MarshalPKIXPublicKey(cryptoKey) + if err != nil { + return nil, fmt.Errorf("error marshalling SSH public key: %v", err) + } + return der, nil +} diff --git a/upup/pkg/fi/cloudup/awstasks/sshkey_test.go b/pkg/pki/sshkey_test.go similarity index 97% rename from upup/pkg/fi/cloudup/awstasks/sshkey_test.go rename to pkg/pki/sshkey_test.go index 28e6052d73..06242f7a7d 100644 --- a/upup/pkg/fi/cloudup/awstasks/sshkey_test.go +++ b/pkg/pki/sshkey_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package awstasks +package pki import ( "fmt" @@ -23,7 +23,7 @@ import ( ) func checkAWSFingerprintEqual(t *testing.T, publicKey string, fingerprint string) { - actual, err := computeAWSKeyFingerprint(publicKey) + actual, err := ComputeAWSKeyFingerprint(publicKey) if err != nil { t.Fatalf("Unexpected error computing AWS key fingerprint: %v", err) } @@ -33,7 +33,7 @@ func checkAWSFingerprintEqual(t *testing.T, publicKey string, fingerprint string } func checkAWSFingerprintError(t *testing.T, publicKey string, message string) { - _, err := computeAWSKeyFingerprint(publicKey) + _, err := ComputeAWSKeyFingerprint(publicKey) if err == nil { t.Fatalf("Expected error %q computing AWS key fingerprint", message) } diff --git a/upup/pkg/fi/cloudup/awstasks/BUILD.bazel b/upup/pkg/fi/cloudup/awstasks/BUILD.bazel index 027128d1f2..24ae0abf7b 100644 --- a/upup/pkg/fi/cloudup/awstasks/BUILD.bazel +++ b/upup/pkg/fi/cloudup/awstasks/BUILD.bazel @@ -66,6 +66,7 @@ go_library( deps = [ "//pkg/diff:go_default_library", "//pkg/featureflag:go_default_library", + "//pkg/pki:go_default_library", "//upup/pkg/fi:go_default_library", "//upup/pkg/fi/cloudup/awsup:go_default_library", "//upup/pkg/fi/cloudup/cloudformation:go_default_library", @@ -80,7 +81,6 @@ go_library( "//vendor/github.com/aws/aws-sdk-go/service/iam:go_default_library", "//vendor/github.com/aws/aws-sdk-go/service/route53:go_default_library", "//vendor/github.com/golang/glog:go_default_library", - "//vendor/golang.org/x/crypto/ssh:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", ], @@ -94,7 +94,6 @@ go_test( "elastic_ip_test.go", "internetgateway_test.go", "securitygroup_test.go", - "sshkey_test.go", "subnet_test.go", "vpc_test.go", ], diff --git a/upup/pkg/fi/cloudup/awstasks/sshkey.go b/upup/pkg/fi/cloudup/awstasks/sshkey.go index c533e77aa5..c734cab4b3 100644 --- a/upup/pkg/fi/cloudup/awstasks/sshkey.go +++ b/upup/pkg/fi/cloudup/awstasks/sshkey.go @@ -17,25 +17,18 @@ limitations under the License. package awstasks import ( - "bytes" - "crypto" - "crypto/md5" - "crypto/rsa" - "crypto/x509" - "encoding/base64" "fmt" - "reflect" "strings" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "github.com/golang/glog" - "golang.org/x/crypto/ssh" + + "k8s.io/kops/pkg/pki" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awsup" "k8s.io/kops/upup/pkg/fi/cloudup/cloudformation" "k8s.io/kops/upup/pkg/fi/cloudup/terraform" - "k8s.io/kops/upup/pkg/fi/utils" ) //go:generate fitask -type=SSHKey @@ -102,100 +95,6 @@ func (e *SSHKey) find(cloud awsup.AWSCloud) (*SSHKey, error) { return actual, nil } -// parseSSHPublicKey parses the SSH public key string -func parseSSHPublicKey(publicKey string) (ssh.PublicKey, error) { - tokens := strings.Fields(publicKey) - if len(tokens) < 2 { - return nil, fmt.Errorf("error parsing SSH public key: %q", publicKey) - } - - sshPublicKeyBytes, err := base64.StdEncoding.DecodeString(tokens[1]) - if err != nil { - return nil, fmt.Errorf("error decoding SSH public key: %q err: %s", publicKey, err) - } - if len(tokens) < 2 { - return nil, fmt.Errorf("error decoding SSH public key: %q", publicKey) - } - - sshPublicKey, err := ssh.ParsePublicKey(sshPublicKeyBytes) - if err != nil { - return nil, fmt.Errorf("error parsing SSH public key: %v", err) - } - return sshPublicKey, nil -} - -// colonSeparatedHex formats the byte slice SSH-fingerprint style: hex bytes separated by colons -func colonSeparatedHex(data []byte) string { - sshKeyFingerprint := fmt.Sprintf("%x", data) - var colonSeparated bytes.Buffer - for i := 0; i < len(sshKeyFingerprint); i++ { - if (i%2) == 0 && i != 0 { - colonSeparated.WriteByte(':') - } - colonSeparated.WriteByte(sshKeyFingerprint[i]) - } - - return colonSeparated.String() -} - -// computeAWSKeyFingerprint computes the AWS-specific fingerprint of the SSH public key -func computeAWSKeyFingerprint(publicKey string) (string, error) { - sshPublicKey, err := parseSSHPublicKey(publicKey) - if err != nil { - return "", err - } - - der, err := toDER(sshPublicKey) - if err != nil { - return "", fmt.Errorf("error computing fingerprint for SSH public key: %v", err) - } - h := md5.Sum(der) - - return colonSeparatedHex(h[:]), nil -} - -// ComputeOpenSSHKeyFingerprint computes the OpenSSH fingerprint of the SSH public key -func ComputeOpenSSHKeyFingerprint(publicKey string) (string, error) { - sshPublicKey, err := parseSSHPublicKey(publicKey) - if err != nil { - return "", err - } - - h := md5.Sum(sshPublicKey.Marshal()) - return colonSeparatedHex(h[:]), nil -} - -// toDER gets the DER encoding of the SSH public key -// Annoyingly, the ssh code wraps the actual crypto keys, so we have to use reflection tricks -func toDER(pubkey ssh.PublicKey) ([]byte, error) { - pubkeyValue := reflect.ValueOf(pubkey) - typeName := utils.BuildTypeName(pubkeyValue.Type()) - - var cryptoKey crypto.PublicKey - switch typeName { - case "*rsaPublicKey": - var rsaPublicKey *rsa.PublicKey - targetType := reflect.ValueOf(rsaPublicKey).Type() - rsaPublicKey = pubkeyValue.Convert(targetType).Interface().(*rsa.PublicKey) - cryptoKey = rsaPublicKey - - //case "*dsaPublicKey": - // var dsaPublicKey *dsa.PublicKey - // targetType := reflect.ValueOf(dsaPublicKey).Type() - // dsaPublicKey = pubkeyValue.Convert(targetType).Interface().(*dsa.PublicKey) - // cryptoKey = dsaPublicKey - - default: - return nil, fmt.Errorf("Unexpected type of SSH key (%q); AWS can only import RSA keys", typeName) - } - - der, err := x509.MarshalPKIXPublicKey(cryptoKey) - if err != nil { - return nil, fmt.Errorf("error marshalling SSH public key: %v", err) - } - return der, nil -} - func (e *SSHKey) Run(c *fi.Context) error { if e.KeyFingerprint == nil && e.PublicKey != nil { publicKey, err := e.PublicKey.AsString() @@ -203,7 +102,7 @@ func (e *SSHKey) Run(c *fi.Context) error { return fmt.Errorf("error reading SSH public key: %v", err) } - keyFingerprint, err := computeAWSKeyFingerprint(publicKey) + keyFingerprint, err := pki.ComputeAWSKeyFingerprint(publicKey) if err != nil { return fmt.Errorf("error computing key fingerprint for SSH key: %v", err) }