From 49241b4f9092982fecfe707a10964ffe56e3b540 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Thu, 30 Jan 2020 13:32:34 +0100 Subject: [PATCH 01/32] Create initial version for signing certs with KMS key --- .gitignore | 1 + cmd/cert.go | 67 ++++++++++++++++++++++++++ sshagent/kms-keyring.go | 6 +-- sshagent/kms-signer.go | 34 ++++++++++---- sshagent/kms-ssh-signer.go | 96 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 cmd/cert.go create mode 100644 sshagent/kms-ssh-signer.go diff --git a/.gitignore b/.gitignore index 58a3a93..728265e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ .vscode /auth-wrapper vendor +/dist diff --git a/cmd/cert.go b/cmd/cert.go new file mode 100644 index 0000000..e3956fd --- /dev/null +++ b/cmd/cert.go @@ -0,0 +1,67 @@ +package main + +import ( + "crypto/rand" + "fmt" + "io/ioutil" + "log" + + "github.com/connectedcars/auth-wrapper/sshagent" + "golang.org/x/crypto/ssh" +) + +// https://medium.com/tarkalabs/ssh-recipes-in-go-an-interlude-6fa88a03d458 +// https://gitlab.openebs.ci/openebs/maya/blob/b5f23e9b2e0c3e9d9503a5c1ae9c15cf8e439db5/vendor/golang.org/x/crypto/ssh/agent/client_test.go +// https://github.com/cloudtools/ssh-cert-authority +// https://github.com/signmykeyio/signmykey + +func signCert(key string) (int, error) { + // Parse public key string + userPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) + if err != nil { + log.Fatal(err) + } + + cert := &ssh.Certificate{ + Key: userPubkey, + KeyId: "test", + CertType: ssh.UserCert, + ValidPrincipals: []string{"tlb"}, + ValidAfter: 0, + ValidBefore: ssh.CertTimeInfinity, // uint64(time.Now().Add(time.Minute * 60).Unix()), + Permissions: ssh.Permissions{ + CriticalOptions: map[string]string{}, + Extensions: map[string]string{}, + }, + } + + /*sshKeyPath := "/Users/f736trbe/.ssh/id_rsa" + privateKeyBytes, err := ioutil.ReadFile(sshKeyPath) + if err != nil { + return 1, fmt.Errorf("Failed to read SSHPrivateKey from %s: %v", sshKeyPath, err) + } + caPrivateKey, err := sshagent.ParsePrivateSSHKey(privateKeyBytes, "") + if err != nil { + return 1, fmt.Errorf("Failed to read SSHPrivateKey from %s: %v", sshKeyPath, err) + } + sshSigner, err := ssh.NewSignerFromKey(caPrivateKey) + if err != nil { + return 1, fmt.Errorf("Failed to read SSHPrivateKey from %s: %v", sshKeyPath, err) + }*/ + + cryptoSigner, err := sshagent.NewKMSSigner("projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3", true) + if err != nil { + return 1, fmt.Errorf("Failed to read NewKMSSigner from: %v", err) + } + sshSigner, err := sshagent.NewSSHSignerFromKMSSigner(cryptoSigner) + if err != nil { + return 1, fmt.Errorf("Failed NewSignerFromSigner from: %v", err) + } + + err = cert.SignCert(rand.Reader, sshSigner) + if err != nil { + return 1, fmt.Errorf("Failed SignCert from %v", err) + } + ioutil.WriteFile("/Users/f736trbe/git/connectedcars/auth-wrapper/cert.pub", ssh.MarshalAuthorizedKey(cert), 0644) + return 1, nil +} diff --git a/sshagent/kms-keyring.go b/sshagent/kms-keyring.go index 437b49c..36f5102 100644 --- a/sshagent/kms-keyring.go +++ b/sshagent/kms-keyring.go @@ -16,7 +16,7 @@ import ( ) type kmsKeyring struct { - signer *KMSSigner + signer KMSSigner locked bool passphrase []byte @@ -27,11 +27,11 @@ var errLocked = errors.New("agent: locked") // NewKMSKeyring returns an Agent that holds keys in memory. It is safe // for concurrent use by multiple goroutines. func NewKMSKeyring(kmsKeyPath string) (sshAgent agent.ExtendedAgent, err error) { - privateKey, err := NewKMSSigner(kmsKeyPath) + privateKey, err := NewKMSSigner(kmsKeyPath, false) if err != nil { return nil, err } - return &kmsKeyring{signer: privateKey.(*KMSSigner)}, nil + return &kmsKeyring{signer: privateKey}, nil } func (r *kmsKeyring) RemoveAll() error { diff --git a/sshagent/kms-signer.go b/sshagent/kms-signer.go index d3076ab..5310a95 100644 --- a/sshagent/kms-signer.go +++ b/sshagent/kms-signer.go @@ -16,14 +16,23 @@ import ( kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" ) -// KMSSigner is a key -type KMSSigner struct { +// KMSSigner is an interface for an opaque private key that can be used for +// signing operations. For example, an RSA key kept in a hardware module. +type KMSSigner interface { + crypto.Signer + Digest() crypto.Hash + SSHPublicKey() ssh.PublicKey +} + +// kmsSigner is a key +type kmsSigner struct { ctx context.Context client *cloudkms.KeyManagementClient keyName string publicKey crypto.PublicKey sshPublicKey ssh.PublicKey digest crypto.Hash + forceDigest bool } // CryptoHashLookup maps crypto.hash to string name @@ -49,7 +58,7 @@ var CryptoHashLookup = map[crypto.Hash]string{ } // NewKMSSigner creates a new instance -func NewKMSSigner(keyName string) (signer crypto.Signer, err error) { +func NewKMSSigner(keyName string, forceDigest bool) (signer KMSSigner, err error) { // Create the KMS client. ctx := context.Background() client, err := cloudkms.NewKeyManagementClient(ctx) @@ -107,13 +116,20 @@ func NewKMSSigner(keyName string) (signer crypto.Signer, err error) { return nil, fmt.Errorf("key %q is not supported format", keyName) } - return &KMSSigner{keyName: keyName, ctx: ctx, client: client, publicKey: publicKey, digest: digestType, sshPublicKey: sshPublicKey}, nil + return &kmsSigner{keyName: keyName, + ctx: ctx, + client: client, + publicKey: publicKey, + digest: digestType, + sshPublicKey: sshPublicKey, + forceDigest: forceDigest, + }, nil } // Sign with key -func (kmss *KMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { +func (kmss *kmsSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { // Check opts to see if the digest algo matches - if opts.HashFunc() != kmss.digest { + if !kmss.forceDigest && opts.HashFunc() != kmss.digest { return nil, fmt.Errorf("Requested hash: %v, supported hash %v", CryptoHashLookup[opts.HashFunc()], CryptoHashLookup[kmss.digest]) } @@ -154,16 +170,16 @@ func (kmss *KMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpt } // Public fetches public key -func (kmss *KMSSigner) Public() crypto.PublicKey { +func (kmss *kmsSigner) Public() crypto.PublicKey { return kmss.publicKey } // SSHPublicKey fetches public key in ssh format -func (kmss *KMSSigner) SSHPublicKey() ssh.PublicKey { +func (kmss *kmsSigner) SSHPublicKey() ssh.PublicKey { return kmss.sshPublicKey } // Digest returns hash algo used for this key -func (kmss *KMSSigner) Digest() crypto.Hash { +func (kmss *kmsSigner) Digest() crypto.Hash { return kmss.digest } diff --git a/sshagent/kms-ssh-signer.go b/sshagent/kms-ssh-signer.go new file mode 100644 index 0000000..ba0c33e --- /dev/null +++ b/sshagent/kms-ssh-signer.go @@ -0,0 +1,96 @@ +package sshagent + +import ( + "crypto" + "encoding/asn1" + "io" + "math/big" + + "golang.org/x/crypto/ssh" +) + +type wrappedSigner struct { + signer KMSSigner + pubKey ssh.PublicKey +} + +// NewSSHSignerFromKMSSigner takes a KMSSigner implementation and +// returns a corresponding ssh.Signer interface. +func NewSSHSignerFromKMSSigner(signer KMSSigner) (ssh.Signer, error) { + pubKey, err := ssh.NewPublicKey(signer.Public()) + if err != nil { + return nil, err + } + return &wrappedSigner{signer, pubKey}, nil +} + +func (s *wrappedSigner) PublicKey() ssh.PublicKey { + return s.pubKey +} + +func (s *wrappedSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) { + return s.SignWithAlgorithm(rand, data) +} + +func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte) (*ssh.Signature, error) { + hashFunc := s.signer.Digest() + + var digest []byte + if hashFunc != 0 { + h := hashFunc.New() + h.Write(data) + digest = h.Sum(nil) + } else { + digest = data + } + + signature, err := s.signer.Sign(rand, digest, hashFunc) + if err != nil { + return nil, err + } + + var algorithm string + if s.PublicKey().Type() == "ssh-rsa" { + switch hashFunc { + case crypto.SHA1: + algorithm = ssh.SigAlgoRSA + case crypto.SHA256: + algorithm = ssh.SigAlgoRSASHA2256 + case crypto.SHA512: + algorithm = ssh.SigAlgoRSASHA2512 + } + } else { + algorithm = s.pubKey.Type() + } + + // crypto.Signer.Sign is expected to return an ASN.1-encoded signature + // for ECDSA and DSA, but that's not the encoding expected by SSH, so + // re-encode. + switch s.pubKey.Type() { + case "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "ssh-dss": + type asn1Signature struct { + R, S *big.Int + } + asn1Sig := new(asn1Signature) + _, err := asn1.Unmarshal(signature, asn1Sig) + if err != nil { + return nil, err + } + + switch s.pubKey.Type() { + case "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521": + signature = ssh.Marshal(asn1Sig) + case "ssh-dss": + signature = make([]byte, 40) + r := asn1Sig.R.Bytes() + s := asn1Sig.S.Bytes() + copy(signature[20-len(r):20], r) + copy(signature[40-len(s):40], s) + } + } + + return &ssh.Signature{ + Format: algorithm, + Blob: signature, + }, nil +} From 7859a40989b00b6d914c98589c71e8a95f16898e Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Sat, 1 Feb 2020 19:20:20 +0100 Subject: [PATCH 02/32] Fix test --- cmd/main_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/main_test.go b/cmd/main_test.go index 44a981c..8fdc90d 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -5,7 +5,9 @@ import ( ) func TestSshAgentWithKMSKey(t *testing.T) { - exitCode, err := runWithSSHAgent("git", []string{"ls-remote", "git@github.com:connectedcars/private-module.git"}, "kms://projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3", "") + exitCode, err := runCommandWithSSHAgent(&SSHAgentConfig{ + userPrivateKeyPath: "kms://projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3" + }, "git", []string{"ls-remote", "git@github.com:connectedcars/private-module.git"}) if err != nil { t.Errorf("Failed with exitCode %v and error %v", exitCode, err) } From 5e7cc17e1e313bffbc2415a68263a2c1f09df1d3 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Sat, 1 Feb 2020 19:29:29 +0100 Subject: [PATCH 03/32] Get initial cert working --- cmd/main.go | 128 +++++++++++++++++++++++-------------- sshagent/kms-keyring.go | 138 +++++++++++++++++++++++----------------- sshagent/kms-signer.go | 3 +- 3 files changed, 164 insertions(+), 105 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e489489..f3bdef8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -66,68 +66,104 @@ func main() { sshKeyPassword := os.Getenv("SSH_KEY_PASSWORD") os.Unsetenv("SSH_KEY_PATH") os.Unsetenv("SSH_KEY_PASSWORD") - exitCode, err := runWithSSHAgent(command, args, sshKeyPath, sshKeyPassword) + + sshCaKeyPath := os.Getenv("SSH_CA_KEY_PATH") + sshCaKeyPassword := os.Getenv("SSH_CA_KEY_PASSWORD") + os.Unsetenv("SSH_CA_KEY_PATH") + os.Unsetenv("SSH_CA_KEY_PASSWORD") + + // Run command with SSH Agent + var exitCode int + var err error + if sshKeyPath != "" { + exitCode, err = runCommandWithSSHAgent(&SSHAgentConfig{ + userPrivateKeyPath: sshKeyPath, + userPrivateKeyPassword: sshKeyPassword, + caPrivateKeyPath: sshCaKeyPath, + caPrivateKeyPassword: sshCaKeyPassword, + }, command, args) + + } else { + exitCode, err = runCommand(command, args) + } if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) } + fmt.Fprintf(os.Stderr, "exit code: %v\n", exitCode) os.Exit(exitCode) } -func runWithSSHAgent(command string, args []string, sshKeyPath string, sshKeyPassword string) (exitCode int, err error) { - var sshAgent agent.Agent - if sshKeyPath != "" { - if strings.HasPrefix(sshKeyPath, "kms://") { - var err error - kmsKeyPath := sshKeyPath[6:] - sshAgent, err = sshagent.NewKMSKeyring(kmsKeyPath) - if err != nil { - return 1, fmt.Errorf("Failed to setup KMS Keyring %s: %v", kmsKeyPath, err) - } - } else { - var privateKey interface{} - privateKeyBytes, err := ioutil.ReadFile(sshKeyPath) - if err != nil { - return 1, fmt.Errorf("Failed to read SSHPrivateKey from %s: %v", sshKeyPath, err) - } - privateKey, err = sshagent.ParsePrivateSSHKey(privateKeyBytes, sshKeyPassword) - if err != nil { - return 1, fmt.Errorf("Failed to read SSHPrivateKey from %s: %v", sshKeyPath, err) - } - sshAgent = agent.NewKeyring() - err = sshAgent.Add(agent.AddedKey{PrivateKey: privateKey, Comment: "my private key"}) - if err != nil { - return 1, err - } - } +// SSHAgentConfig holds the config for the SSH Agent +type SSHAgentConfig struct { + userPrivateKeyPath string + userPrivateKeyPassword string + caPrivateKeyPath string + caPrivateKeyPassword string +} - // Print loaded keys - keyList, err := sshAgent.List() +func runCommandWithSSHAgent(config *SSHAgentConfig, command string, args []string) (exitCode int, err error) { + agent, err := createSSHAgent(config) + if err != nil { + return 255, fmt.Errorf("Failed to setup ssh agent: %v\n", err) + } + + sshAuthSock, err := sshagent.StartSSHAgentServer(agent) + if err != nil { + return 255, fmt.Errorf("Failed to start ssh agent server: %v", err) + } + fmt.Fprintf(os.Stderr, "Setting SSH_AUTH_SOCK using ssh key: %s\n", config.userPrivateKeyPath) + os.Setenv("SSH_AUTH_SOCK", sshAuthSock) + + // Do string replacement for SSH_AUTH_SOCK + for i, arg := range args { + args[i] = strings.ReplaceAll(arg, "$SSH_AUTH_SOCK", sshAuthSock) + args[i] = strings.ReplaceAll(args[i], "$$SSH_AUTH_SOCK", sshAuthSock) + } + + // Print loaded keys + keyList, err := agent.List() + if err != nil { + return 255, fmt.Errorf("Failed to list sshAgent keys %s: %v", config.userPrivateKeyPath, err) + } + fmt.Fprintf(os.Stderr, "Loaded keys:\n") + for _, key := range keyList { + fmt.Fprintf(os.Stderr, "%s %s\n", strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(key)), "\n"), key.Comment) + } + + return runCommand(command, args) +} + +func createSSHAgent(config *SSHAgentConfig) (sshAgent agent.Agent, err error) { + // TODO: Support mixing keys + if strings.HasPrefix(config.userPrivateKeyPath, "kms://") && strings.HasPrefix(config.caPrivateKeyPath, "kms://") { + var err error + userPrivateKeyPath := config.userPrivateKeyPath[6:] + caPrivateKeyPath := config.caPrivateKeyPath[6:] + sshAgent, err = sshagent.NewKMSKeyring(userPrivateKeyPath, caPrivateKeyPath) if err != nil { - return 1, fmt.Errorf("Failed to list sshAgent keys %s: %v", sshKeyPath, err) + return nil, fmt.Errorf("Failed to setup KMS Keyring %s: %v", userPrivateKeyPath, err) } - - fmt.Fprintf(os.Stderr, "Loaded keys:\n") - for _, key := range keyList { - fmt.Fprintf(os.Stderr, "%s\n", string(ssh.MarshalAuthorizedKey(key))) + } else { + var privateKey interface{} + privateKeyBytes, err := ioutil.ReadFile(config.userPrivateKeyPath) + if err != nil { + return nil, fmt.Errorf("Failed to read user SSH private key from %s: %v", config.userPrivateKeyPath, err) } - - sshAuthSock, err := sshagent.StartSSHAgentServer(sshAgent) + privateKey, err = sshagent.ParsePrivateSSHKey(privateKeyBytes, config.userPrivateKeyPassword) if err != nil { - return 1, fmt.Errorf("Failed to start ssh agent server: %v", err) + return nil, fmt.Errorf("Failed to read or decrypt SSH private key from %s: %v", config.userPrivateKeyPath, err) } - fmt.Fprintf(os.Stderr, "Setting SSH_AUTH_SOCK using ssh key: %s\n", sshKeyPath) - os.Setenv("SSH_AUTH_SOCK", sshAuthSock) - - // Do string replacement for SSH_AUTH_SOCK - for i, arg := range args { - //fmt.Fprintf(os.Stderr, "arg[%d]: %s\n", i, arg) - args[i] = strings.ReplaceAll(arg, "$SSH_AUTH_SOCK", sshAuthSock) - args[i] = strings.ReplaceAll(args[i], "$$SSH_AUTH_SOCK", sshAuthSock) + sshAgent = agent.NewKeyring() + err = sshAgent.Add(agent.AddedKey{PrivateKey: privateKey, Comment: "my private key"}) + if err != nil { + return nil, err } - } + return sshAgent, nil +} +func runCommand(command string, args []string) (exitCode int, err error) { cmd := exec.Command(command, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/sshagent/kms-keyring.go b/sshagent/kms-keyring.go index 36f5102..c9fb17c 100644 --- a/sshagent/kms-keyring.go +++ b/sshagent/kms-keyring.go @@ -2,12 +2,7 @@ package sshagent import ( "bytes" - "crypto" - "crypto/dsa" - "crypto/ecdsa" - "crypto/ed25519" "crypto/rand" - "crypto/rsa" "errors" "fmt" @@ -16,7 +11,12 @@ import ( ) type kmsKeyring struct { - signer KMSSigner + userPrivateKeyPath string + caPrivateKeyPath string + userSigner KMSSigner + userSSHSigner ssh.Signer + caSigner KMSSigner + caSSHSigner ssh.Signer locked bool passphrase []byte @@ -26,12 +26,33 @@ var errLocked = errors.New("agent: locked") // NewKMSKeyring returns an Agent that holds keys in memory. It is safe // for concurrent use by multiple goroutines. -func NewKMSKeyring(kmsKeyPath string) (sshAgent agent.ExtendedAgent, err error) { - privateKey, err := NewKMSSigner(kmsKeyPath, false) +func NewKMSKeyring(userPrivateKeyPath string, caPrivateKeyPath string) (sshAgent agent.ExtendedAgent, err error) { + userPrivateKey, err := NewKMSSigner(userPrivateKeyPath, false) if err != nil { return nil, err } - return &kmsKeyring{signer: privateKey}, nil + userSSHSigner, err := NewSSHSignerFromKMSSigner(userPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) + } + + caPrivateKey, err := NewKMSSigner(caPrivateKeyPath, false) + if err != nil { + return nil, err + } + caSSHSigner, err := NewSSHSignerFromKMSSigner(caPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) + } + + return &kmsKeyring{ + userPrivateKeyPath: userPrivateKeyPath, + caPrivateKeyPath: caPrivateKeyPath, + userSigner: userPrivateKey, + caSigner: caPrivateKey, + userSSHSigner: userSSHSigner, + caSSHSigner: caSSHSigner, + }, nil } func (r *kmsKeyring) RemoveAll() error { @@ -64,15 +85,58 @@ func (r *kmsKeyring) Extension(extensionType string, contents []byte) ([]byte, e return nil, agent.ErrExtensionUnsupported } +// SSHCertificate adds support for modern hash type +type SSHCertificate struct { + ssh.Certificate +} + +// Type returns the key name. It is part of the PublicKey interface. +func (c *SSHCertificate) Type() string { + return "rsa-sha2-512-cert-v01@openssh.com" +} + // List returns the identities known to the agent. func (r *kmsKeyring) List() ([]*agent.Key, error) { var ids []*agent.Key - pub := r.signer.SSHPublicKey() + userPublicKey := r.userSigner.SSHPublicKey() + ids = append(ids, &agent.Key{ + Format: userPublicKey.Type(), + Blob: userPublicKey.Marshal(), + Comment: "user " + r.userPrivateKeyPath}) + + // Add the CA public key so it's easy to copy paste + caPublicKey := r.caSigner.SSHPublicKey() + ids = append(ids, &agent.Key{ + Format: caPublicKey.Type(), + Blob: caPublicKey.Marshal(), + Comment: "ca " + r.caPrivateKeyPath}) + + // Sign and add a user certificate to the keyring + userCert := &SSHCertificate{ssh.Certificate{ + Key: userPublicKey, + KeyId: "test", + CertType: ssh.UserCert, + ValidPrincipals: []string{"tlb"}, + ValidAfter: 0, + ValidBefore: ssh.CertTimeInfinity, // uint64(time.Now().Add(time.Minute * 60).Unix()), + Permissions: ssh.Permissions{ + CriticalOptions: map[string]string{}, + Extensions: map[string]string{}, + }, + }} + err := userCert.SignCert(rand.Reader, r.caSSHSigner) + if err != nil { + return nil, fmt.Errorf("failed SignCert from %v", err) + } + + // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com + // To fix this we would need to replace the keyname in the certBlob with one of the names listed. + certBlob := userCert.Marshal() ids = append(ids, &agent.Key{ - Format: pub.Type(), - Blob: pub.Marshal(), - Comment: "my kms key"}) + Format: userCert.Type(), + Blob: certBlob, + Comment: "user cert " + r.userPrivateKeyPath}) return ids, nil } @@ -85,55 +149,13 @@ func (r *kmsKeyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error func (r *kmsKeyring) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { wanted := key.Marshal() - if bytes.Equal(r.signer.SSHPublicKey().Marshal(), wanted) { + if bytes.Equal(r.userSigner.SSHPublicKey().Marshal(), wanted) { // Ignore flags as they google key only supports one type of hashing. - - // Generate digest - var digest []byte - h := r.signer.Digest().New() - h.Write(data) - digest = h.Sum(nil) - - // Sign the digest - signature, err := r.signer.Sign(rand.Reader, digest, r.signer.Digest()) + signature, err := r.userSSHSigner.Sign(rand.Reader, data) if err != nil { return nil, err } - - var algorithm string - switch r.signer.Public().(type) { - case *dsa.PublicKey: - algorithm = ssh.KeyAlgoDSA // Not support by KMS - case *rsa.PublicKey: - switch r.signer.Digest() { - case crypto.SHA1: // Not support by KMS - algorithm = ssh.SigAlgoRSA - case crypto.SHA256: - algorithm = ssh.SigAlgoRSASHA2256 - case crypto.SHA512: - algorithm = ssh.SigAlgoRSASHA2512 - default: - return nil, fmt.Errorf("Unknown digest type %v", CryptoHashLookup[r.signer.Digest()]) - } - case *ecdsa.PublicKey: - switch r.signer.Digest() { - case crypto.SHA256: - algorithm = ssh.KeyAlgoECDSA256 - case crypto.SHA384: - algorithm = ssh.KeyAlgoECDSA384 - case crypto.SHA512: - algorithm = ssh.KeyAlgoECDSA521 - default: - return nil, fmt.Errorf("Unknown digest type %v", CryptoHashLookup[r.signer.Digest()]) - } - case *ed25519.PublicKey: - algorithm = ssh.KeyAlgoED25519 - } - - return &ssh.Signature{ - Format: algorithm, - Blob: signature, - }, nil + return signature, nil } return nil, errors.New("not found") diff --git a/sshagent/kms-signer.go b/sshagent/kms-signer.go index 5310a95..d489247 100644 --- a/sshagent/kms-signer.go +++ b/sshagent/kms-signer.go @@ -116,7 +116,8 @@ func NewKMSSigner(keyName string, forceDigest bool) (signer KMSSigner, err error return nil, fmt.Errorf("key %q is not supported format", keyName) } - return &kmsSigner{keyName: keyName, + return &kmsSigner{ + keyName: keyName, ctx: ctx, client: client, publicKey: publicKey, From cbd15451417d9b362de45f2784bdd03a1783b9c0 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Sun, 9 Feb 2020 22:36:29 +0100 Subject: [PATCH 04/32] Get initial logic for signing service working --- cmd/main.go | 4 ++ server/http.go | 46 ++++++++++++ server/main.go | 152 ++++++++++++++++++++++++++++++++++++++++ server/utils.go | 15 ++++ sshagent/kms-keyring.go | 56 ++++++++------- sshagent/kms-signer.go | 2 + 6 files changed, 250 insertions(+), 25 deletions(-) create mode 100644 server/http.go create mode 100644 server/main.go create mode 100644 server/utils.go diff --git a/cmd/main.go b/cmd/main.go index f3bdef8..ff2096e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -16,6 +16,10 @@ import ( "golang.org/x/crypto/ssh/agent" ) +// authwrapper ssh 1.2.3.4 +// Please write reason for login: +// + func main() { processName := filepath.Base(os.Args[0]) var command string diff --git a/server/http.go b/server/http.go new file mode 100644 index 0000000..8035f4d --- /dev/null +++ b/server/http.go @@ -0,0 +1,46 @@ +package server + +import ( + "net/http" + + "golang.org/x/crypto/ssh" +) + +// HTTPSigningServer is a http server +type HTTPSigningServer struct { + signingServer *SigningServer +} + +// NewHTTPSigningServer returns a HTTPSigningServer +func NewHTTPSigningServer(caKey ssh.Signer) (httpSigningServer *HTTPSigningServer, err error) { + signingServer := NewSigningServer(caKey) + httpSigningServer = &HTTPSigningServer{signingServer: signingServer} + + http.Handle("/", httpSigningServer) + err = http.ListenAndServe(":8080", nil) + if err != nil { + return nil, err + } + return httpSigningServer, nil +} + +func (s *HTTPSigningServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + location := r.Method + " " + r.URL.Path + + switch location { + case "GET /certificate/challenge": + s.getCertificateChallenge(w, r) + case "POST /certificate": + s.postCertificate(w, r) + } + +} + +func (s *HTTPSigningServer) getCertificateChallenge(w http.ResponseWriter, r *http.Request) { + +} + +func (s *HTTPSigningServer) postCertificate(w http.ResponseWriter, r *http.Request) { + +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..09cedd5 --- /dev/null +++ b/server/main.go @@ -0,0 +1,152 @@ +package server + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "io" + "strings" + + "time" + + "golang.org/x/crypto/ssh" +) + +// Challenge is a JSON structure for signing +type Challenge struct { + Value []byte `json:"value"` + Signature *ssh.Signature `json:"signature"` +} + +type challengeValue struct { + timestamp time.Time + random []byte +} + +// CertificateRequest for SSH user certificate +type CertificateRequest struct { + Challenge *Challenge `json:"challenge"` + Command string `json:"command"` + Args []string `json:"args"` + PublicKey string `json:"publicKey"` + Signature *ssh.Signature `json:"signature"` +} + +// SignRequest signs request with provided user key : Move to common lib as this is used by the client +func (s *CertificateRequest) SignRequest(rand io.Reader, userKey ssh.Signer) (err error) { + payload := GenerateSigningPayload(s) + signature, err := userKey.Sign(rand, payload) + if err != nil { + return err + } + s.Signature = signature + return nil +} + +// CertificateResponse is the signed user certificate +type CertificateResponse struct { + Certificate string `json:"certificate"` +} + +// SigningServer struct +type SigningServer struct { + caKey ssh.Signer +} + +// NewSigningServer creates a new server +func NewSigningServer(caKey ssh.Signer) *SigningServer { + return &SigningServer{caKey: caKey} +} + +// VerifyCertificateRequest errors if it fails validation +func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest) (err error) { + // Validate challenge came from us + challenge := certRequest.Challenge + err = s.caKey.PublicKey().Verify(challenge.Value, challenge.Signature) + if err != nil { + return err + } + + // Unpack the value and ensure it's still valid + var value challengeValue + err = json.Unmarshal(challenge.Value, value) + if err != nil { + return err + } + + // TODO: Check if challenge expired + // TODO: Look up public key instead of parsing it + userPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certRequest.PublicKey)) + if err != nil { + return err + } + + payload := GenerateSigningPayload(certRequest) + + // Verify that public key signed it + err = userPubkey.Verify(payload, certRequest.Signature) + if err != nil { + return err + } + + return nil +} + +// IssueUserCertificate issues ssh user certificate +func (s *SigningServer) IssueUserCertificate(userPublicKey ssh.PublicKey) (userCertificate *ssh.Certificate, err error) { + userCert := &ssh.Certificate{ + Key: userPublicKey, + KeyId: "test", + CertType: ssh.UserCert, + ValidPrincipals: []string{"tlb"}, + ValidAfter: 0, + ValidBefore: ssh.CertTimeInfinity, // uint64(time.Now().Add(time.Minute * 60).Unix()), + Permissions: ssh.Permissions{ + CriticalOptions: map[string]string{}, + Extensions: map[string]string{}, + }, + } + + // Sign and add a user certificate to the keyring + err = userCert.SignCert(rand.Reader, s.caKey) + if err != nil { + return nil, fmt.Errorf("failed SignCert from %v", err) + } + + return userCert, err +} + +// GenerateSigningPayload generates payload for signing +func GenerateSigningPayload(certRequest *CertificateRequest) (payload []byte) { + challenge := certRequest.Challenge + // Build the signed payload + payload = challenge.Value + payload = append(payload, challenge.Signature.Format...) + payload = append(payload, challenge.Signature.Blob...) + payload = append(payload, certRequest.Command...) + payload = append(payload, strings.Join(certRequest.Args, "")...) + return payload +} + +// GenerateChallenge creates a challenge payload for signing +func (s *SigningServer) GenerateChallenge() (challenge *Challenge, err error) { + randomBytes, err := GenerateRamdomBytes(40) + if err != nil { + return nil, err + } + + jsonBytes, err := json.Marshal(challengeValue{ + timestamp: time.Now(), + random: randomBytes, + }) + if err != nil { + return nil, err + } + + signature, err := s.caKey.Sign(rand.Reader, jsonBytes) + if err != nil { + return nil, err + } + + return &Challenge{Value: jsonBytes, Signature: signature}, nil +} diff --git a/server/utils.go b/server/utils.go new file mode 100644 index 0000000..04459f4 --- /dev/null +++ b/server/utils.go @@ -0,0 +1,15 @@ +package server + +import ( + "crypto/rand" +) + +// GenerateRamdomBytes from cryptographically secure source +func GenerateRamdomBytes(length int) (value []byte, err error) { + randomBytes := make([]byte, length) + _, err = rand.Read(randomBytes) + if err != nil { + return nil, err + } + return randomBytes, nil +} diff --git a/sshagent/kms-keyring.go b/sshagent/kms-keyring.go index c9fb17c..90b565b 100644 --- a/sshagent/kms-keyring.go +++ b/sshagent/kms-keyring.go @@ -1,11 +1,14 @@ package sshagent +// TODO: Make generic so it can be used with other key implementation + import ( "bytes" "crypto/rand" "errors" "fmt" + "github.com/connectedcars/auth-wrapper/server" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" ) @@ -85,16 +88,6 @@ func (r *kmsKeyring) Extension(extensionType string, contents []byte) ([]byte, e return nil, agent.ErrExtensionUnsupported } -// SSHCertificate adds support for modern hash type -type SSHCertificate struct { - ssh.Certificate -} - -// Type returns the key name. It is part of the PublicKey interface. -func (c *SSHCertificate) Type() string { - return "rsa-sha2-512-cert-v01@openssh.com" -} - // List returns the identities known to the agent. func (r *kmsKeyring) List() ([]*agent.Key, error) { var ids []*agent.Key @@ -112,25 +105,38 @@ func (r *kmsKeyring) List() ([]*agent.Key, error) { Blob: caPublicKey.Marshal(), Comment: "ca " + r.caPrivateKeyPath}) - // Sign and add a user certificate to the keyring - userCert := &SSHCertificate{ssh.Certificate{ - Key: userPublicKey, - KeyId: "test", - CertType: ssh.UserCert, - ValidPrincipals: []string{"tlb"}, - ValidAfter: 0, - ValidBefore: ssh.CertTimeInfinity, // uint64(time.Now().Add(time.Minute * 60).Unix()), - Permissions: ssh.Permissions{ - CriticalOptions: map[string]string{}, - Extensions: map[string]string{}, - }, - }} - err := userCert.SignCert(rand.Reader, r.caSSHSigner) + signingServer := server.NewSigningServer(r.caSSHSigner) + + // TODO: Make HTTP request to get a signed certificate + // 1. GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } + challenge, err := signingServer.GenerateChallenge() + if err != nil { + // TODO: Error handling, fx. hard fail + } + + // 2. POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } + certRequest := &server.CertificateRequest{ + Challenge: challenge, + Command: "some command", // TODO: Get command + Args: []string{}, // TODO: Get args + PublicKey: string(ssh.MarshalAuthorizedKey(userPublicKey)), + } + certRequest.SignRequest(rand.Reader, r.userSSHSigner) + + // # Client: sign(challenge + command + args) Server: pubkey.verify(challenge + command + args, signature) + err = signingServer.VerifyCertificateRequest(certRequest) + if err != nil { + // TODO: Error handling + } + + // Get back { certificate: "base64 encoded cert" } + userCert, err := signingServer.IssueUserCertificate(r.userSigner.SSHPublicKey()) if err != nil { - return nil, fmt.Errorf("failed SignCert from %v", err) + // TODO: Error handling } // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com + // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD // To fix this we would need to replace the keyname in the certBlob with one of the names listed. certBlob := userCert.Marshal() ids = append(ids, &agent.Key{ diff --git a/sshagent/kms-signer.go b/sshagent/kms-signer.go index d489247..3399d9e 100644 --- a/sshagent/kms-signer.go +++ b/sshagent/kms-signer.go @@ -1,5 +1,7 @@ package sshagent +// TODO: Move to google kms package instead + import ( "context" "crypto" From 89db0175c6ce908f6ebf50235240d80359668016 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Sun, 9 Feb 2020 23:07:56 +0100 Subject: [PATCH 05/32] Fix challengeValue json marshaling --- server/main.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/main.go b/server/main.go index 09cedd5..0885923 100644 --- a/server/main.go +++ b/server/main.go @@ -19,8 +19,8 @@ type Challenge struct { } type challengeValue struct { - timestamp time.Time - random []byte + Timestamp string `json:"timestamp"` + Random []byte `json:"random"` } // CertificateRequest for SSH user certificate @@ -69,7 +69,7 @@ func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest // Unpack the value and ensure it's still valid var value challengeValue - err = json.Unmarshal(challenge.Value, value) + err = json.Unmarshal(challenge.Value, &value) if err != nil { return err } @@ -135,10 +135,11 @@ func (s *SigningServer) GenerateChallenge() (challenge *Challenge, err error) { return nil, err } - jsonBytes, err := json.Marshal(challengeValue{ - timestamp: time.Now(), - random: randomBytes, + jsonBytes, err := json.Marshal(&challengeValue{ + Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05.999Z"), + Random: randomBytes, }) + if err != nil { return nil, err } From 7b5c16c0b8b40e3e2e56a28ed1b850330b39817f Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Fri, 8 May 2020 00:10:09 +0200 Subject: [PATCH 06/32] Get http signing server working --- cmd/main.go | 20 ++++++++++- server/http.go | 71 +++++++++++++++++++++++++++++++------ server/main.go | 16 +++++---- sshagent/kms-keyring.go | 77 ++++++++++++++++++++++++++++++++--------- 4 files changed, 150 insertions(+), 34 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index ff2096e..7b5fa04 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "io/ioutil" + "log" "os" "os/exec" "path/filepath" @@ -11,6 +12,7 @@ import ( "cloud.google.com/go/compute/metadata" "github.com/connectedcars/auth-wrapper/gcemetadata" + "github.com/connectedcars/auth-wrapper/server" "github.com/connectedcars/auth-wrapper/sshagent" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" @@ -139,12 +141,28 @@ func runCommandWithSSHAgent(config *SSHAgentConfig, command string, args []strin } func createSSHAgent(config *SSHAgentConfig) (sshAgent agent.Agent, err error) { + // TODO: Support mixing keys if strings.HasPrefix(config.userPrivateKeyPath, "kms://") && strings.HasPrefix(config.caPrivateKeyPath, "kms://") { var err error userPrivateKeyPath := config.userPrivateKeyPath[6:] caPrivateKeyPath := config.caPrivateKeyPath[6:] - sshAgent, err = sshagent.NewKMSKeyring(userPrivateKeyPath, caPrivateKeyPath) + + // Start the signing server + caPrivateKey, err := sshagent.NewKMSSigner(caPrivateKeyPath, false) + if err != nil { + return nil, err + } + caSSHSigner, err := sshagent.NewSSHSignerFromKMSSigner(caPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) + } + go func() { + log.Fatal(server.StartHTTPSigningServer(caSSHSigner, ":3080")) + }() + + // Setup sshAgent + sshAgent, err = sshagent.NewKMSKeyring(userPrivateKeyPath, caPrivateKeyPath, "http://localhost:3080") if err != nil { return nil, fmt.Errorf("Failed to setup KMS Keyring %s: %v", userPrivateKeyPath, err) } diff --git a/server/http.go b/server/http.go index 8035f4d..8f61081 100644 --- a/server/http.go +++ b/server/http.go @@ -1,46 +1,95 @@ package server import ( + "encoding/json" + "io/ioutil" "net/http" "golang.org/x/crypto/ssh" ) +// StatusError is returned for http errors +type StatusError struct { + Code int + Err error +} + // HTTPSigningServer is a http server type HTTPSigningServer struct { signingServer *SigningServer } -// NewHTTPSigningServer returns a HTTPSigningServer -func NewHTTPSigningServer(caKey ssh.Signer) (httpSigningServer *HTTPSigningServer, err error) { +// StartHTTPSigningServer returns a HTTPSigningServer +func StartHTTPSigningServer(caKey ssh.Signer, listenAddr string) error { signingServer := NewSigningServer(caKey) - httpSigningServer = &HTTPSigningServer{signingServer: signingServer} + httpSigningServer := &HTTPSigningServer{signingServer: signingServer} http.Handle("/", httpSigningServer) - err = http.ListenAndServe(":8080", nil) + err := http.ListenAndServe(listenAddr, nil) if err != nil { - return nil, err + return err } - return httpSigningServer, nil + return nil } func (s *HTTPSigningServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") location := r.Method + " " + r.URL.Path + var errorStatus *StatusError + var jsonResponse interface{} switch location { case "GET /certificate/challenge": - s.getCertificateChallenge(w, r) + jsonResponse, errorStatus = s.getCertificateChallenge(w, r) case "POST /certificate": - s.postCertificate(w, r) + jsonResponse, errorStatus = s.postCertificate(w, r) + default: + http.Error(w, "Not found", 400) + } + if errorStatus != nil { + http.Error(w, errorStatus.Err.Error(), errorStatus.Code) } + if jsonResponse != nil { + jsonBytes, err := json.Marshal(jsonResponse) + if err != nil { + http.Error(w, err.Error(), 500) + } + w.Write(jsonBytes) + } } -func (s *HTTPSigningServer) getCertificateChallenge(w http.ResponseWriter, r *http.Request) { - +func (s *HTTPSigningServer) getCertificateChallenge(w http.ResponseWriter, r *http.Request) (jsonResponse interface{}, statusError *StatusError) { + challenge, err := s.signingServer.GenerateChallenge() + if err != nil { + return nil, &StatusError{500, err} + } + return challenge, nil } -func (s *HTTPSigningServer) postCertificate(w http.ResponseWriter, r *http.Request) { +func (s *HTTPSigningServer) postCertificate(w http.ResponseWriter, r *http.Request) (jsonResponse interface{}, statusError *StatusError) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, &StatusError{500, err} + } + + var certRequest CertificateRequest + err = json.Unmarshal(body, &certRequest) + if err != nil { + return nil, &StatusError{400, err} + } + + userPublickey, err := s.signingServer.VerifyCertificateRequest(&certRequest) + if err != nil { + return nil, &StatusError{400, err} + } + + userCert, err := s.signingServer.IssueUserCertificate(userPublickey) + if err != nil { + return nil, &StatusError{500, err} + } + userCertString := ssh.MarshalAuthorizedKey(userCert) + return &CertificateResponse{Certificate: string(userCertString)}, nil } diff --git a/server/main.go b/server/main.go index 0885923..00853a8 100644 --- a/server/main.go +++ b/server/main.go @@ -59,26 +59,30 @@ func NewSigningServer(caKey ssh.Signer) *SigningServer { } // VerifyCertificateRequest errors if it fails validation -func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest) (err error) { +func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest) (pubkey ssh.PublicKey, err error) { // Validate challenge came from us challenge := certRequest.Challenge + if challenge == nil { + return nil, fmt.Errorf("Challenge not set") + } + err = s.caKey.PublicKey().Verify(challenge.Value, challenge.Signature) if err != nil { - return err + return nil, err } // Unpack the value and ensure it's still valid var value challengeValue err = json.Unmarshal(challenge.Value, &value) if err != nil { - return err + return nil, err } // TODO: Check if challenge expired // TODO: Look up public key instead of parsing it userPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certRequest.PublicKey)) if err != nil { - return err + return nil, err } payload := GenerateSigningPayload(certRequest) @@ -86,10 +90,10 @@ func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest // Verify that public key signed it err = userPubkey.Verify(payload, certRequest.Signature) if err != nil { - return err + return nil, err } - return nil + return userPubkey, nil } // IssueUserCertificate issues ssh user certificate diff --git a/sshagent/kms-keyring.go b/sshagent/kms-keyring.go index 90b565b..4c3f869 100644 --- a/sshagent/kms-keyring.go +++ b/sshagent/kms-keyring.go @@ -5,8 +5,13 @@ package sshagent import ( "bytes" "crypto/rand" + "encoding/json" "errors" "fmt" + "io" + "io/ioutil" + "net/http" + "time" "github.com/connectedcars/auth-wrapper/server" "golang.org/x/crypto/ssh" @@ -20,6 +25,8 @@ type kmsKeyring struct { userSSHSigner ssh.Signer caSigner KMSSigner caSSHSigner ssh.Signer + signingServerURL string + signingHTTPClient *http.Client locked bool passphrase []byte @@ -29,7 +36,7 @@ var errLocked = errors.New("agent: locked") // NewKMSKeyring returns an Agent that holds keys in memory. It is safe // for concurrent use by multiple goroutines. -func NewKMSKeyring(userPrivateKeyPath string, caPrivateKeyPath string) (sshAgent agent.ExtendedAgent, err error) { +func NewKMSKeyring(userPrivateKeyPath string, caPrivateKeyPath string, signingServerURL string) (sshAgent agent.ExtendedAgent, err error) { userPrivateKey, err := NewKMSSigner(userPrivateKeyPath, false) if err != nil { return nil, err @@ -48,6 +55,8 @@ func NewKMSKeyring(userPrivateKeyPath string, caPrivateKeyPath string) (sshAgent return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) } + signingHTTPClient := &http.Client{Timeout: 10 * time.Second} + return &kmsKeyring{ userPrivateKeyPath: userPrivateKeyPath, caPrivateKeyPath: caPrivateKeyPath, @@ -55,6 +64,8 @@ func NewKMSKeyring(userPrivateKeyPath string, caPrivateKeyPath string) (sshAgent caSigner: caPrivateKey, userSSHSigner: userSSHSigner, caSSHSigner: caSSHSigner, + signingHTTPClient: signingHTTPClient, + signingServerURL: signingServerURL, }, nil } @@ -105,35 +116,34 @@ func (r *kmsKeyring) List() ([]*agent.Key, error) { Blob: caPublicKey.Marshal(), Comment: "ca " + r.caPrivateKeyPath}) - signingServer := server.NewSigningServer(r.caSSHSigner) - - // TODO: Make HTTP request to get a signed certificate - // 1. GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } - challenge, err := signingServer.GenerateChallenge() + // GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } + var challenge server.Challenge + err := r.httpSignRequest("GET", "/certificate/challenge", nil, &challenge) if err != nil { - // TODO: Error handling, fx. hard fail + return nil, err } - // 2. POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } + // POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } certRequest := &server.CertificateRequest{ - Challenge: challenge, + Challenge: &challenge, Command: "some command", // TODO: Get command Args: []string{}, // TODO: Get args PublicKey: string(ssh.MarshalAuthorizedKey(userPublicKey)), } + // sign(challenge + command + args) certRequest.SignRequest(rand.Reader, r.userSSHSigner) - // # Client: sign(challenge + command + args) Server: pubkey.verify(challenge + command + args, signature) - err = signingServer.VerifyCertificateRequest(certRequest) + // get back { certificate: "base64 encoded cert" } + var certResponse server.CertificateResponse + err = r.httpSignRequest("POST", "/certificate", certRequest, &certResponse) if err != nil { - // TODO: Error handling + return nil, err } - - // Get back { certificate: "base64 encoded cert" } - userCert, err := signingServer.IssueUserCertificate(r.userSigner.SSHPublicKey()) + userCertPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certResponse.Certificate)) if err != nil { - // TODO: Error handling + return nil, nil } + userCert := userCertPubkey.(*ssh.Certificate) // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD @@ -166,3 +176,38 @@ func (r *kmsKeyring) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.S return nil, errors.New("not found") } + +func (r *kmsKeyring) httpSignRequest(method string, url string, request interface{}, response interface{}) error { + // Convert request to JSON and wrap in io.Reader + var requestBody io.Reader + if request != nil { + jsonBytes, err := json.Marshal(request) + if err != nil { + return err + } + requestBody = bytes.NewReader(jsonBytes) + } + + // Do Request and ready body + challengeRequest, err := http.NewRequest(method, r.signingServerURL+url, requestBody) + if err != nil { + return err + } + challengeResponse, err := r.signingHTTPClient.Do(challengeRequest) + if err != nil { + return err + } + defer challengeResponse.Body.Close() + responseBody, err := ioutil.ReadAll(challengeResponse.Body) + if err != nil { + return err + } + + // Convert JSON to object + err = json.Unmarshal(responseBody, response) + if err != nil { + return err + } + + return nil +} From 6f5c32ec6303eb2ad3e633b227cabd4182bb24cb Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Fri, 8 May 2020 00:11:05 +0200 Subject: [PATCH 07/32] Fix linting --- cmd/main.go | 2 +- cmd/main_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 7b5fa04..9aaa55b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -111,7 +111,7 @@ type SSHAgentConfig struct { func runCommandWithSSHAgent(config *SSHAgentConfig, command string, args []string) (exitCode int, err error) { agent, err := createSSHAgent(config) if err != nil { - return 255, fmt.Errorf("Failed to setup ssh agent: %v\n", err) + return 255, fmt.Errorf("failed to setup ssh agent: %v", err) } sshAuthSock, err := sshagent.StartSSHAgentServer(agent) diff --git a/cmd/main_test.go b/cmd/main_test.go index 8fdc90d..c10ddbf 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -6,7 +6,7 @@ import ( func TestSshAgentWithKMSKey(t *testing.T) { exitCode, err := runCommandWithSSHAgent(&SSHAgentConfig{ - userPrivateKeyPath: "kms://projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3" + userPrivateKeyPath: "kms://projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3", }, "git", []string{"ls-remote", "git@github.com:connectedcars/private-module.git"}) if err != nil { t.Errorf("Failed with exitCode %v and error %v", exitCode, err) From 0193674934dffeea435ca8902654c25c34abb42d Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Thu, 14 May 2020 22:20:29 +0200 Subject: [PATCH 08/32] Restructure --- cmd/cert.go | 67 ---------------------- cmd/main.go | 11 +++- {sshagent => kms/google}/kms-keyring.go | 2 +- {sshagent => kms/google}/kms-signer.go | 2 +- {sshagent => kms/google}/kms-ssh-signer.go | 2 +- 5 files changed, 11 insertions(+), 73 deletions(-) delete mode 100644 cmd/cert.go rename {sshagent => kms/google}/kms-keyring.go (99%) rename {sshagent => kms/google}/kms-signer.go (99%) rename {sshagent => kms/google}/kms-ssh-signer.go (99%) diff --git a/cmd/cert.go b/cmd/cert.go deleted file mode 100644 index e3956fd..0000000 --- a/cmd/cert.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "crypto/rand" - "fmt" - "io/ioutil" - "log" - - "github.com/connectedcars/auth-wrapper/sshagent" - "golang.org/x/crypto/ssh" -) - -// https://medium.com/tarkalabs/ssh-recipes-in-go-an-interlude-6fa88a03d458 -// https://gitlab.openebs.ci/openebs/maya/blob/b5f23e9b2e0c3e9d9503a5c1ae9c15cf8e439db5/vendor/golang.org/x/crypto/ssh/agent/client_test.go -// https://github.com/cloudtools/ssh-cert-authority -// https://github.com/signmykeyio/signmykey - -func signCert(key string) (int, error) { - // Parse public key string - userPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) - if err != nil { - log.Fatal(err) - } - - cert := &ssh.Certificate{ - Key: userPubkey, - KeyId: "test", - CertType: ssh.UserCert, - ValidPrincipals: []string{"tlb"}, - ValidAfter: 0, - ValidBefore: ssh.CertTimeInfinity, // uint64(time.Now().Add(time.Minute * 60).Unix()), - Permissions: ssh.Permissions{ - CriticalOptions: map[string]string{}, - Extensions: map[string]string{}, - }, - } - - /*sshKeyPath := "/Users/f736trbe/.ssh/id_rsa" - privateKeyBytes, err := ioutil.ReadFile(sshKeyPath) - if err != nil { - return 1, fmt.Errorf("Failed to read SSHPrivateKey from %s: %v", sshKeyPath, err) - } - caPrivateKey, err := sshagent.ParsePrivateSSHKey(privateKeyBytes, "") - if err != nil { - return 1, fmt.Errorf("Failed to read SSHPrivateKey from %s: %v", sshKeyPath, err) - } - sshSigner, err := ssh.NewSignerFromKey(caPrivateKey) - if err != nil { - return 1, fmt.Errorf("Failed to read SSHPrivateKey from %s: %v", sshKeyPath, err) - }*/ - - cryptoSigner, err := sshagent.NewKMSSigner("projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3", true) - if err != nil { - return 1, fmt.Errorf("Failed to read NewKMSSigner from: %v", err) - } - sshSigner, err := sshagent.NewSSHSignerFromKMSSigner(cryptoSigner) - if err != nil { - return 1, fmt.Errorf("Failed NewSignerFromSigner from: %v", err) - } - - err = cert.SignCert(rand.Reader, sshSigner) - if err != nil { - return 1, fmt.Errorf("Failed SignCert from %v", err) - } - ioutil.WriteFile("/Users/f736trbe/git/connectedcars/auth-wrapper/cert.pub", ssh.MarshalAuthorizedKey(cert), 0644) - return 1, nil -} diff --git a/cmd/main.go b/cmd/main.go index 9aaa55b..888858a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,6 +12,7 @@ import ( "cloud.google.com/go/compute/metadata" "github.com/connectedcars/auth-wrapper/gcemetadata" + "github.com/connectedcars/auth-wrapper/kms/google" "github.com/connectedcars/auth-wrapper/server" "github.com/connectedcars/auth-wrapper/sshagent" "golang.org/x/crypto/ssh" @@ -78,6 +79,10 @@ func main() { os.Unsetenv("SSH_CA_KEY_PATH") os.Unsetenv("SSH_CA_KEY_PASSWORD") + if sshCaKeyPath != "" { + + } + // Run command with SSH Agent var exitCode int var err error @@ -149,11 +154,11 @@ func createSSHAgent(config *SSHAgentConfig) (sshAgent agent.Agent, err error) { caPrivateKeyPath := config.caPrivateKeyPath[6:] // Start the signing server - caPrivateKey, err := sshagent.NewKMSSigner(caPrivateKeyPath, false) + caPrivateKey, err := google.NewKMSSigner(caPrivateKeyPath, false) if err != nil { return nil, err } - caSSHSigner, err := sshagent.NewSSHSignerFromKMSSigner(caPrivateKey) + caSSHSigner, err := google.NewSSHSignerFromKMSSigner(caPrivateKey) if err != nil { return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) } @@ -162,7 +167,7 @@ func createSSHAgent(config *SSHAgentConfig) (sshAgent agent.Agent, err error) { }() // Setup sshAgent - sshAgent, err = sshagent.NewKMSKeyring(userPrivateKeyPath, caPrivateKeyPath, "http://localhost:3080") + sshAgent, err = google.NewKMSKeyring(userPrivateKeyPath, caPrivateKeyPath, "http://localhost:3080") if err != nil { return nil, fmt.Errorf("Failed to setup KMS Keyring %s: %v", userPrivateKeyPath, err) } diff --git a/sshagent/kms-keyring.go b/kms/google/kms-keyring.go similarity index 99% rename from sshagent/kms-keyring.go rename to kms/google/kms-keyring.go index 4c3f869..17efbef 100644 --- a/sshagent/kms-keyring.go +++ b/kms/google/kms-keyring.go @@ -1,4 +1,4 @@ -package sshagent +package google // TODO: Make generic so it can be used with other key implementation diff --git a/sshagent/kms-signer.go b/kms/google/kms-signer.go similarity index 99% rename from sshagent/kms-signer.go rename to kms/google/kms-signer.go index 3399d9e..8dcb507 100644 --- a/sshagent/kms-signer.go +++ b/kms/google/kms-signer.go @@ -1,4 +1,4 @@ -package sshagent +package google // TODO: Move to google kms package instead diff --git a/sshagent/kms-ssh-signer.go b/kms/google/kms-ssh-signer.go similarity index 99% rename from sshagent/kms-ssh-signer.go rename to kms/google/kms-ssh-signer.go index ba0c33e..7face86 100644 --- a/sshagent/kms-ssh-signer.go +++ b/kms/google/kms-ssh-signer.go @@ -1,4 +1,4 @@ -package sshagent +package google import ( "crypto" From bea20eda1cb0f0083df88c99c0eb5fc4cbfbc56d Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Thu, 14 May 2020 23:29:48 +0200 Subject: [PATCH 09/32] Remove dependency on KSMSigner --- kms/google/kms-keyring.go | 99 +++++++++++++++------------------------ 1 file changed, 39 insertions(+), 60 deletions(-) diff --git a/kms/google/kms-keyring.go b/kms/google/kms-keyring.go index 17efbef..cfb86e9 100644 --- a/kms/google/kms-keyring.go +++ b/kms/google/kms-keyring.go @@ -21,10 +21,7 @@ import ( type kmsKeyring struct { userPrivateKeyPath string caPrivateKeyPath string - userSigner KMSSigner userSSHSigner ssh.Signer - caSigner KMSSigner - caSSHSigner ssh.Signer signingServerURL string signingHTTPClient *http.Client @@ -36,7 +33,7 @@ var errLocked = errors.New("agent: locked") // NewKMSKeyring returns an Agent that holds keys in memory. It is safe // for concurrent use by multiple goroutines. -func NewKMSKeyring(userPrivateKeyPath string, caPrivateKeyPath string, signingServerURL string) (sshAgent agent.ExtendedAgent, err error) { +func NewKMSKeyring(userPrivateKeyPath string, signingServerURL string) (sshAgent agent.ExtendedAgent, err error) { userPrivateKey, err := NewKMSSigner(userPrivateKeyPath, false) if err != nil { return nil, err @@ -46,24 +43,11 @@ func NewKMSKeyring(userPrivateKeyPath string, caPrivateKeyPath string, signingSe return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) } - caPrivateKey, err := NewKMSSigner(caPrivateKeyPath, false) - if err != nil { - return nil, err - } - caSSHSigner, err := NewSSHSignerFromKMSSigner(caPrivateKey) - if err != nil { - return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) - } - signingHTTPClient := &http.Client{Timeout: 10 * time.Second} return &kmsKeyring{ userPrivateKeyPath: userPrivateKeyPath, - caPrivateKeyPath: caPrivateKeyPath, - userSigner: userPrivateKey, - caSigner: caPrivateKey, userSSHSigner: userSSHSigner, - caSSHSigner: caSSHSigner, signingHTTPClient: signingHTTPClient, signingServerURL: signingServerURL, }, nil @@ -103,56 +87,51 @@ func (r *kmsKeyring) Extension(extensionType string, contents []byte) ([]byte, e func (r *kmsKeyring) List() ([]*agent.Key, error) { var ids []*agent.Key - userPublicKey := r.userSigner.SSHPublicKey() + userPublicKey := r.userSSHSigner.PublicKey() ids = append(ids, &agent.Key{ Format: userPublicKey.Type(), Blob: userPublicKey.Marshal(), Comment: "user " + r.userPrivateKeyPath}) - // Add the CA public key so it's easy to copy paste - caPublicKey := r.caSigner.SSHPublicKey() - ids = append(ids, &agent.Key{ - Format: caPublicKey.Type(), - Blob: caPublicKey.Marshal(), - Comment: "ca " + r.caPrivateKeyPath}) - - // GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } - var challenge server.Challenge - err := r.httpSignRequest("GET", "/certificate/challenge", nil, &challenge) - if err != nil { - return nil, err - } + if r.signingServerURL != "" { + // GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } + var challenge server.Challenge + err := r.httpSignRequest("GET", "/certificate/challenge", nil, &challenge) + if err != nil { + return nil, err + } - // POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } - certRequest := &server.CertificateRequest{ - Challenge: &challenge, - Command: "some command", // TODO: Get command - Args: []string{}, // TODO: Get args - PublicKey: string(ssh.MarshalAuthorizedKey(userPublicKey)), - } - // sign(challenge + command + args) - certRequest.SignRequest(rand.Reader, r.userSSHSigner) + // POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } + certRequest := &server.CertificateRequest{ + Challenge: &challenge, + Command: "some command", // TODO: Get command + Args: []string{}, // TODO: Get args + PublicKey: string(ssh.MarshalAuthorizedKey(userPublicKey)), + } + // sign(challenge + command + args) + certRequest.SignRequest(rand.Reader, r.userSSHSigner) - // get back { certificate: "base64 encoded cert" } - var certResponse server.CertificateResponse - err = r.httpSignRequest("POST", "/certificate", certRequest, &certResponse) - if err != nil { - return nil, err - } - userCertPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certResponse.Certificate)) - if err != nil { - return nil, nil + // get back { certificate: "base64 encoded cert" } + var certResponse server.CertificateResponse + err = r.httpSignRequest("POST", "/certificate", certRequest, &certResponse) + if err != nil { + return nil, err + } + userCertPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certResponse.Certificate)) + if err != nil { + return nil, nil + } + userCert := userCertPubkey.(*ssh.Certificate) + + // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com + // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD + // To fix this we would need to replace the keyname in the certBlob with one of the names listed. + certBlob := userCert.Marshal() + ids = append(ids, &agent.Key{ + Format: userCert.Type(), + Blob: certBlob, + Comment: "user cert " + r.userPrivateKeyPath}) } - userCert := userCertPubkey.(*ssh.Certificate) - - // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com - // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD - // To fix this we would need to replace the keyname in the certBlob with one of the names listed. - certBlob := userCert.Marshal() - ids = append(ids, &agent.Key{ - Format: userCert.Type(), - Blob: certBlob, - Comment: "user cert " + r.userPrivateKeyPath}) return ids, nil } @@ -165,7 +144,7 @@ func (r *kmsKeyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error func (r *kmsKeyring) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { wanted := key.Marshal() - if bytes.Equal(r.userSigner.SSHPublicKey().Marshal(), wanted) { + if bytes.Equal(r.userSSHSigner.PublicKey().Marshal(), wanted) { // Ignore flags as they google key only supports one type of hashing. signature, err := r.userSSHSigner.Sign(rand.Reader, data) if err != nil { From edf9f96859be37c23475f6103d10af0fc0a0a0f8 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Thu, 14 May 2020 23:30:12 +0200 Subject: [PATCH 10/32] Cleanup --- cmd/main.go | 91 +++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 888858a..a2a5f2d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,8 +10,6 @@ import ( "strings" "syscall" - "cloud.google.com/go/compute/metadata" - "github.com/connectedcars/auth-wrapper/gcemetadata" "github.com/connectedcars/auth-wrapper/kms/google" "github.com/connectedcars/auth-wrapper/server" "github.com/connectedcars/auth-wrapper/sshagent" @@ -57,48 +55,40 @@ func main() { args = os.Args[2:] } - gceMetaDataURL := os.Getenv("GCE_METADATA_URL") - if gceMetaDataURL == "auto" { - if metadata.OnGCE() { - gcemetadata.StartMetadateServer("http://169.254.169.254") - } - } else if gceMetaDataURL == "emulate" { - // Start emulation server - gcemetadata.StartMetadateServer("") - } else if gceMetaDataURL != "" { - gcemetadata.StartMetadateServer(gceMetaDataURL) - } - - sshKeyPath := os.Getenv("SSH_KEY_PATH") - sshKeyPassword := os.Getenv("SSH_KEY_PASSWORD") - os.Unsetenv("SSH_KEY_PATH") - os.Unsetenv("SSH_KEY_PASSWORD") - sshCaKeyPath := os.Getenv("SSH_CA_KEY_PATH") sshCaKeyPassword := os.Getenv("SSH_CA_KEY_PASSWORD") + sshSigningServerAddress := os.Getenv("SSH_SIGNING_SERVER_LISTEN_ADDRESS") os.Unsetenv("SSH_CA_KEY_PATH") os.Unsetenv("SSH_CA_KEY_PASSWORD") - - if sshCaKeyPath != "" { - + os.Unsetenv("SSH_SIGNING_SERVER_LISTEN_ADDRESS") + if sshCaKeyPath != "" && sshSigningServerAddress != "" { + caPublickey, err := createSigningServer(sshCaKeyPath, sshCaKeyPassword, sshSigningServerAddress) + if err != nil { + log.Fatalf("createSigningServer: %v", err) + } + fmt.Fprintf(os.Stderr, "%s %s\n", strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(caPublickey)), "\n"), "ca "+sshCaKeyPath) } // Run command with SSH Agent + sshKeyPath := os.Getenv("SSH_KEY_PATH") + sshKeyPassword := os.Getenv("SSH_KEY_PASSWORD") + sshSigningServerURL := os.Getenv("SSH_SIGNING_SERVER_URL") + os.Unsetenv("SSH_KEY_PATH") + os.Unsetenv("SSH_KEY_PASSWORD") var exitCode int var err error if sshKeyPath != "" { exitCode, err = runCommandWithSSHAgent(&SSHAgentConfig{ userPrivateKeyPath: sshKeyPath, userPrivateKeyPassword: sshKeyPassword, - caPrivateKeyPath: sshCaKeyPath, - caPrivateKeyPassword: sshCaKeyPassword, + sshSigningServerURL: sshSigningServerURL, }, command, args) } else { exitCode, err = runCommand(command, args) } if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + log.Fatalf("runCommand: %v", err) } fmt.Fprintf(os.Stderr, "exit code: %v\n", exitCode) @@ -109,8 +99,7 @@ func main() { type SSHAgentConfig struct { userPrivateKeyPath string userPrivateKeyPassword string - caPrivateKeyPath string - caPrivateKeyPassword string + sshSigningServerURL string } func runCommandWithSSHAgent(config *SSHAgentConfig, command string, args []string) (exitCode int, err error) { @@ -146,32 +135,18 @@ func runCommandWithSSHAgent(config *SSHAgentConfig, command string, args []strin } func createSSHAgent(config *SSHAgentConfig) (sshAgent agent.Agent, err error) { - // TODO: Support mixing keys - if strings.HasPrefix(config.userPrivateKeyPath, "kms://") && strings.HasPrefix(config.caPrivateKeyPath, "kms://") { + if strings.HasPrefix(config.userPrivateKeyPath, "kms://") { var err error userPrivateKeyPath := config.userPrivateKeyPath[6:] - caPrivateKeyPath := config.caPrivateKeyPath[6:] - - // Start the signing server - caPrivateKey, err := google.NewKMSSigner(caPrivateKeyPath, false) - if err != nil { - return nil, err - } - caSSHSigner, err := google.NewSSHSignerFromKMSSigner(caPrivateKey) - if err != nil { - return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) - } - go func() { - log.Fatal(server.StartHTTPSigningServer(caSSHSigner, ":3080")) - }() // Setup sshAgent - sshAgent, err = google.NewKMSKeyring(userPrivateKeyPath, caPrivateKeyPath, "http://localhost:3080") + sshAgent, err = google.NewKMSKeyring(userPrivateKeyPath, config.sshSigningServerURL) if err != nil { return nil, fmt.Errorf("Failed to setup KMS Keyring %s: %v", userPrivateKeyPath, err) } } else { + // TODO: Create generic keyring that takes array of ssh.Signer's var privateKey interface{} privateKeyBytes, err := ioutil.ReadFile(config.userPrivateKeyPath) if err != nil { @@ -190,6 +165,34 @@ func createSSHAgent(config *SSHAgentConfig) (sshAgent agent.Agent, err error) { return sshAgent, nil } +func createSigningServer(caPrivateKeyPath string, sshCaKeyPassword string, address string) (ssh.PublicKey, error) { + var caPublicKey ssh.PublicKey + if strings.HasPrefix(caPrivateKeyPath, "kms://") { + var err error + kmsCaPrivateKeyPath := caPrivateKeyPath[6:] + + // Start the signing server + caPrivateKey, err := google.NewKMSSigner(kmsCaPrivateKeyPath, false) + if err != nil { + return nil, fmt.Errorf("failed google.NewKMSSigner %v", err) + } + caSSHSigner, err := google.NewSSHSignerFromKMSSigner(caPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed google.NewSignerFromSigner from: %v", err) + } + + go func() { + log.Fatal(server.StartHTTPSigningServer(caSSHSigner, address)) + }() + caPublicKey = caPrivateKey.SSHPublicKey() + + } else { + return nil, fmt.Errorf("Not implemented yet") + } + + return caPublicKey, nil +} + func runCommand(command string, args []string) (exitCode int, err error) { cmd := exec.Command(command, args...) cmd.Stdin = os.Stdin From 05f479896848cfe5b30b5663860d2f1aad1794bf Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Thu, 14 May 2020 23:30:34 +0200 Subject: [PATCH 11/32] Add debug info for cloudbuilder --- cloudbuild.yaml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index f98522f..3650d27 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -2,18 +2,12 @@ steps: # Pull a modern version of docker - name: 'gcr.io/cloud-builders/docker' args: ['pull', 'gcr.io/cloud-builders/docker:latest'] - # Workaround for https://github.com/moby/moby/issues/39120 - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'docker/dockerfile:experimental'] - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'docker/dockerfile:1.0-experimental'] - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'docker.io/docker/dockerfile-copy:v0.1.9'] - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'ubuntu:19.10'] # Check version - name: 'gcr.io/cloud-builders/docker' args: ['version'] + - name: 'gcr.io/cloud-builders/docker' + args: ['-c', 'find $HOME'] + entrypoint: "/bin/bash" # # Build KMS auth wrappers # From cee78e66054c4d921cc6320656e8a34edfa4d441 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Thu, 14 May 2020 23:32:29 +0200 Subject: [PATCH 12/32] Escape --- cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 3650d27..72a5f3c 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -6,7 +6,7 @@ steps: - name: 'gcr.io/cloud-builders/docker' args: ['version'] - name: 'gcr.io/cloud-builders/docker' - args: ['-c', 'find $HOME'] + args: ['-c', 'find $$HOME'] entrypoint: "/bin/bash" # # Build KMS auth wrappers From 2f9f1269391092954dda54374f3d845f7af8d784 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Thu, 14 May 2020 23:34:57 +0200 Subject: [PATCH 13/32] Revert workarounds --- cloudbuild.yaml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 72a5f3c..d9fe62a 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -2,11 +2,20 @@ steps: # Pull a modern version of docker - name: 'gcr.io/cloud-builders/docker' args: ['pull', 'gcr.io/cloud-builders/docker:latest'] + # Workaround for https://github.com/moby/moby/issues/39120 + - name: 'gcr.io/cloud-builders/docker' + args: ['pull', 'docker/dockerfile:experimental'] + - name: 'gcr.io/cloud-builders/docker' + args: ['pull', 'docker/dockerfile:1.0-experimental'] + - name: 'gcr.io/cloud-builders/docker' + args: ['pull', 'docker.io/docker/dockerfile-copy:v0.1.9'] + - name: 'gcr.io/cloud-builders/docker' + args: ['pull', 'ubuntu:19.10'] # Check version - name: 'gcr.io/cloud-builders/docker' args: ['version'] - - name: 'gcr.io/cloud-builders/docker' - args: ['-c', 'find $$HOME'] + - name: 'gcr.io/cloud-builders/gsutil' + args: ['-c', 'find $$HOME && export'] entrypoint: "/bin/bash" # # Build KMS auth wrappers From 7ee72dc1a381ddf814ce362283a6b40786baca4b Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Fri, 15 May 2020 23:16:43 +0200 Subject: [PATCH 14/32] Use ssh.AlgorithmSigner as the basis for the keyring to make things a lot simpler --- cmd/authwrapper/main.go | 95 +++++++++++++ cmd/{ => authwrapper}/main_test.go | 7 +- cmd/authwrapper/setup.go | 159 +++++++++++++++++++++ cmd/authwrapper/utils.go | 126 +++++++++++++++++ cmd/main.go | 217 ----------------------------- gcemetadata/main.go | 100 ------------- kms/google/kms-keyring.go | 192 ------------------------- kms/google/kms-signer.go | 38 ++--- kms/google/kms-ssh-signer.go | 41 +++--- sshagent/keyring.go | 118 ++++++++++++++++ sshagent/sshagent-ssh-signer.go | 43 ++++++ sshagent/sshagent.go | 10 ++ 12 files changed, 587 insertions(+), 559 deletions(-) create mode 100644 cmd/authwrapper/main.go rename cmd/{ => authwrapper}/main_test.go (87%) create mode 100644 cmd/authwrapper/setup.go create mode 100644 cmd/authwrapper/utils.go delete mode 100644 cmd/main.go delete mode 100644 gcemetadata/main.go delete mode 100644 kms/google/kms-keyring.go create mode 100644 sshagent/keyring.go create mode 100644 sshagent/sshagent-ssh-signer.go diff --git a/cmd/authwrapper/main.go b/cmd/authwrapper/main.go new file mode 100644 index 0000000..6d2f217 --- /dev/null +++ b/cmd/authwrapper/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "golang.org/x/crypto/ssh" +) + +func main() { + processName := filepath.Base(os.Args[0]) + + config, err := parseEnvironment() + if err != nil { + log.Fatalf(": %v", err) + } + + var command string + var args []string + if config.WrapCommand != "" { + command = config.WrapCommand + args = os.Args[1:] + } else if processName != "auth-wrapper" && processName != "__debug_bin" { + // Get executable path + ex, err := os.Executable() + if err != nil { + panic(err) + } + processPath := filepath.Dir(ex) + + // Remove wrapper location path + currentPath := os.Getenv("PATH") + cleanedPath := strings.Replace(currentPath, processPath+"/:", "", 1) + cleanedPath = strings.Replace(cleanedPath, processPath+":", "", 1) + os.Setenv("PATH", cleanedPath) + + command = processName + args = os.Args[1:] + } else { + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "auth-wrapper cmd args") + os.Exit(1) + } + // Setup exec command + command = os.Args[1] + args = os.Args[2:] + } + + if config.SSHCaKeyPath != "" && config.SSHSigningServerAddress != "" { + caPublickey, err := startSigningServer( + config.SSHCaKeyPath, + config.SSHCaKeyPassword, + config.SSHSigningServerAddress, + ) + if err != nil { + log.Fatalf("createSigningServer: %v", err) + } + pubkeyString := strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(caPublickey)), "\n") + fmt.Fprintf(os.Stderr, "%s %s\n", pubkeyString, "ca "+config.SSHCaKeyPath) + } + + var exitCode int + if config.SSHKeyPath != "" || config.SSHAgentSocket != "" { + agent, err := setupKeyring(config) + if err != nil { + log.Fatalf("Failed to setup keyring: %v", err) + } + + // List loaded keys + keyList, err := agent.List() + if err != nil { + log.Fatalf("Failed to list sshAgent keys: %v", err) + } + fmt.Fprintf(os.Stderr, "Loaded keys:\n") + for _, key := range keyList { + fmt.Fprintf(os.Stderr, "%s %s\n", strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(key)), "\n"), key.Comment) + } + + exitCode, err = runCommandWithSSHAgent(agent, command, args) + if err != nil { + log.Fatalf("runCommandWithSSHAgent: %v", err) + } + } else { + exitCode, err = runCommand(command, args) + if err != nil { + log.Fatalf("runCommand: %v", err) + } + } + + fmt.Fprintf(os.Stderr, "exit code: %v\n", exitCode) + os.Exit(exitCode) +} diff --git a/cmd/main_test.go b/cmd/authwrapper/main_test.go similarity index 87% rename from cmd/main_test.go rename to cmd/authwrapper/main_test.go index c10ddbf..d6f87ad 100644 --- a/cmd/main_test.go +++ b/cmd/authwrapper/main_test.go @@ -1,10 +1,6 @@ package main -import ( - "testing" -) - -func TestSshAgentWithKMSKey(t *testing.T) { +/*func TestSshAgentWithKMSKey(t *testing.T) { exitCode, err := runCommandWithSSHAgent(&SSHAgentConfig{ userPrivateKeyPath: "kms://projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3", }, "git", []string{"ls-remote", "git@github.com:connectedcars/private-module.git"}) @@ -15,3 +11,4 @@ func TestSshAgentWithKMSKey(t *testing.T) { t.Errorf("Failed with exitCode %v", exitCode) } } +*/ diff --git a/cmd/authwrapper/setup.go b/cmd/authwrapper/setup.go new file mode 100644 index 0000000..43e7534 --- /dev/null +++ b/cmd/authwrapper/setup.go @@ -0,0 +1,159 @@ +package main + +import ( + "crypto/rand" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/connectedcars/auth-wrapper/kms/google" + "github.com/connectedcars/auth-wrapper/server" + "github.com/connectedcars/auth-wrapper/sshagent" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +// Config contains the auth wrapper configuration +type Config struct { + WrapCommand string + SSHKeyPath string + SSHKeyPassword string + SSHSigningServerURL string + SSHCaKeyPath string + SSHCaKeyPassword string + SSHSigningServerAddress string + SSHAgentSocket string +} + +func parseEnvironment() (*Config, error) { + config := &Config{ + WrapCommand: os.Getenv("WRAP_COMMAND"), + SSHKeyPath: os.Getenv("SSH_KEY_PATH"), + SSHKeyPassword: os.Getenv("SSH_KEY_PASSWORD"), + SSHSigningServerURL: os.Getenv("SSH_SIGNING_SERVER_URL"), + SSHCaKeyPath: os.Getenv("SSH_CA_KEY_PATH"), + SSHCaKeyPassword: os.Getenv("SSH_CA_KEY_PASSWORD"), + SSHSigningServerAddress: os.Getenv("SSH_SIGNING_SERVER_LISTEN_ADDRESS"), + SSHAgentSocket: os.Getenv("SSH_AUTH_SOCK"), + } + os.Unsetenv("WRAP_COMMAND") + os.Unsetenv("SSH_KEY_PATH") + os.Unsetenv("SSH_KEY_PASSWORD") + os.Unsetenv("SSH_SIGNING_SERVER_URL") + os.Unsetenv("SSH_CA_KEY_PATH") + os.Unsetenv("SSH_CA_KEY_PASSWORD") + os.Unsetenv("SSH_SIGNING_SERVER_LISTEN_ADDRESS") + os.Unsetenv("SSH_AUTH_SOCK") + + // TODO: Do basic error validation + + return config, nil +} + +func setupKeyring(config *Config) (agent.ExtendedAgent, error) { + var signers []sshagent.SSHAlgorithmSigner + var certificates []sshagent.SSHCertificate + + if config.SSHKeyPath != "" { + var userSigner ssh.AlgorithmSigner + if strings.HasPrefix(config.SSHKeyPath, "kms://") { + var err error + userPrivateKeyPath := config.SSHKeyPath[6:] + userPrivateKey, err := google.NewKMSSigner(userPrivateKeyPath, false) + if err != nil { + return nil, err + } + signer, err := google.NewSSHSignerFromSigner(userPrivateKey, userPrivateKey.Digest()) + if err != nil { + return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) + } + signers = append(signers, sshagent.SSHAlgorithmSigner{ + Signer: signer, + Comment: "google kms key " + userPrivateKeyPath, + }) + userSigner = signer + } else { + privateKeyBytes, err := ioutil.ReadFile(config.SSHKeyPath) + if err != nil { + return nil, fmt.Errorf("Failed to read user SSH private key from %s: %v", config.SSHKeyPath, err) + } + + var privateKey interface{} + privateKey, err = sshagent.ParsePrivateSSHKey(privateKeyBytes, config.SSHKeyPath) + if err != nil { + return nil, fmt.Errorf("Failed to read or decrypt SSH private key from %s: %v", config.SSHKeyPath, err) + } + signer, err := ssh.NewSignerFromKey(privateKey) + + algorithmSigner, ok := signer.(ssh.AlgorithmSigner) + if !ok { + return nil, fmt.Errorf("signature does not support non-default signature algorithm: %T", signer) + } + signers = append(signers, sshagent.SSHAlgorithmSigner{ + Signer: algorithmSigner, + Comment: "local key " + config.SSHKeyPath, + }) + userSigner = algorithmSigner + } + + if config.SSHSigningServerURL != "" { + // GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } + var challenge server.Challenge + err := httpJSONRequest("GET", config.SSHSigningServerURL+"/certificate/challenge", nil, &challenge) + if err != nil { + return nil, err + } + + // POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } + certRequest := &server.CertificateRequest{ + Challenge: &challenge, + Command: "some command", // TODO: Get command + Args: []string{}, // TODO: Get args + PublicKey: string(ssh.MarshalAuthorizedKey(userSigner.PublicKey())), + } + // sign(challenge + command + args) + certRequest.SignRequest(rand.Reader, userSigner) + + // get back { certificate: "base64 encoded cert" } + var certResponse server.CertificateResponse + err = httpJSONRequest("POST", config.SSHSigningServerURL+"/certificate", certRequest, &certResponse) + if err != nil { + return nil, err + } + userCertPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certResponse.Certificate)) + if err != nil { + return nil, nil + } + userCert := userCertPubkey.(*ssh.Certificate) + + certificates = append(certificates, sshagent.SSHCertificate{ + Certificate: userCert, + Comment: "user key " + config.SSHKeyPath, + }) + } + + } + + if config.SSHAgentSocket != "" { + agent, err := sshagent.ConnectSSHAgent(config.SSHAgentSocket) + if err != nil { + return nil, err + } + + keys, err := agent.List() + if err != nil { + return nil, err + } + + for _, key := range keys { + signer := sshagent.NewSSHAlgorithmSigner(agent, key) + signers = append(signers, sshagent.SSHAlgorithmSigner{ + Signer: signer, + Comment: "agent key", + }) + } + } + + return sshagent.NewSSHAlgorithmSignerKeyring(&signers, &certificates) +} diff --git a/cmd/authwrapper/utils.go b/cmd/authwrapper/utils.go new file mode 100644 index 0000000..54ced79 --- /dev/null +++ b/cmd/authwrapper/utils.go @@ -0,0 +1,126 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "strings" + "syscall" + "time" + + "github.com/connectedcars/auth-wrapper/kms/google" + "github.com/connectedcars/auth-wrapper/server" + "github.com/connectedcars/auth-wrapper/sshagent" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +var httpClient = &http.Client{Timeout: 10 * time.Second} + +func runCommandWithSSHAgent(agent agent.ExtendedAgent, command string, args []string) (exitCode int, err error) { + sshAuthSock, err := sshagent.StartSSHAgentServer(agent) + if err != nil { + return 255, fmt.Errorf("Failed to start ssh agent server: %v", err) + } + os.Setenv("SSH_AUTH_SOCK", sshAuthSock) + + // Do string replacement for SSH_AUTH_SOCK + for i, arg := range args { + args[i] = strings.ReplaceAll(arg, "$SSH_AUTH_SOCK", sshAuthSock) + args[i] = strings.ReplaceAll(args[i], "$$SSH_AUTH_SOCK", sshAuthSock) + } + + return runCommand(command, args) +} + +func startSigningServer(caPrivateKeyPath string, sshCaKeyPassword string, address string) (ssh.PublicKey, error) { + var caPublicKey ssh.PublicKey + if strings.HasPrefix(caPrivateKeyPath, "kms://") { + var err error + kmsCaPrivateKeyPath := caPrivateKeyPath[6:] + + // Start the signing server + caPrivateKey, err := google.NewKMSSigner(kmsCaPrivateKeyPath, false) + if err != nil { + return nil, fmt.Errorf("failed google.NewKMSSigner %v", err) + } + caSSHSigner, err := google.NewSSHSignerFromSigner(caPrivateKey, caPrivateKey.Digest()) + if err != nil { + return nil, fmt.Errorf("failed google.NewSignerFromSigner from: %v", err) + } + + go func() { + log.Fatal(server.StartHTTPSigningServer(caSSHSigner, address)) + }() + caPublicKey = caSSHSigner.PublicKey() + + } else { + return nil, fmt.Errorf("Not implemented yet") + } + + return caPublicKey, nil +} + +func runCommand(command string, args []string) (exitCode int, err error) { + cmd := exec.Command(command, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return 1, fmt.Errorf("cmd.Start: %v", err) + } + + err = cmd.Wait() + if err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + return status.ExitStatus(), nil + } + return 1, fmt.Errorf("Failed to get status code: %v", err) + } + return 1, fmt.Errorf("cmd.Wait: %v", err) + } + return 0, nil +} + +func httpJSONRequest(method string, url string, request interface{}, response interface{}) error { + // Convert request to JSON and wrap in io.Reader + var requestBody io.Reader + if request != nil { + jsonBytes, err := json.Marshal(request) + if err != nil { + return err + } + requestBody = bytes.NewReader(jsonBytes) + } + + // Do Request and ready body + challengeRequest, err := http.NewRequest(method, url, requestBody) + if err != nil { + return err + } + challengeResponse, err := httpClient.Do(challengeRequest) + if err != nil { + return err + } + defer challengeResponse.Body.Close() + responseBody, err := ioutil.ReadAll(challengeResponse.Body) + if err != nil { + return err + } + + // Convert JSON to object + err = json.Unmarshal(responseBody, response) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index a2a5f2d..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,217 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "syscall" - - "github.com/connectedcars/auth-wrapper/kms/google" - "github.com/connectedcars/auth-wrapper/server" - "github.com/connectedcars/auth-wrapper/sshagent" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" -) - -// authwrapper ssh 1.2.3.4 -// Please write reason for login: -// - -func main() { - processName := filepath.Base(os.Args[0]) - var command string - var args []string - wrapCommand := os.Getenv("WRAP_COMMAND") - if wrapCommand != "" { - command = wrapCommand - args = os.Args[1:] - } else if processName != "auth-wrapper" && processName != "__debug_bin" { - // Get executable path - ex, err := os.Executable() - if err != nil { - panic(err) - } - processPath := filepath.Dir(ex) - - // Remove wrapper location path - currentPath := os.Getenv("PATH") - cleanedPath := strings.Replace(currentPath, processPath+"/:", "", 1) - cleanedPath = strings.Replace(cleanedPath, processPath+":", "", 1) - os.Setenv("PATH", cleanedPath) - - command = processName - args = os.Args[1:] - } else { - if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "auth-wrapper cmd args") - os.Exit(1) - } - // Setup exec command - command = os.Args[1] - args = os.Args[2:] - } - - sshCaKeyPath := os.Getenv("SSH_CA_KEY_PATH") - sshCaKeyPassword := os.Getenv("SSH_CA_KEY_PASSWORD") - sshSigningServerAddress := os.Getenv("SSH_SIGNING_SERVER_LISTEN_ADDRESS") - os.Unsetenv("SSH_CA_KEY_PATH") - os.Unsetenv("SSH_CA_KEY_PASSWORD") - os.Unsetenv("SSH_SIGNING_SERVER_LISTEN_ADDRESS") - if sshCaKeyPath != "" && sshSigningServerAddress != "" { - caPublickey, err := createSigningServer(sshCaKeyPath, sshCaKeyPassword, sshSigningServerAddress) - if err != nil { - log.Fatalf("createSigningServer: %v", err) - } - fmt.Fprintf(os.Stderr, "%s %s\n", strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(caPublickey)), "\n"), "ca "+sshCaKeyPath) - } - - // Run command with SSH Agent - sshKeyPath := os.Getenv("SSH_KEY_PATH") - sshKeyPassword := os.Getenv("SSH_KEY_PASSWORD") - sshSigningServerURL := os.Getenv("SSH_SIGNING_SERVER_URL") - os.Unsetenv("SSH_KEY_PATH") - os.Unsetenv("SSH_KEY_PASSWORD") - var exitCode int - var err error - if sshKeyPath != "" { - exitCode, err = runCommandWithSSHAgent(&SSHAgentConfig{ - userPrivateKeyPath: sshKeyPath, - userPrivateKeyPassword: sshKeyPassword, - sshSigningServerURL: sshSigningServerURL, - }, command, args) - - } else { - exitCode, err = runCommand(command, args) - } - if err != nil { - log.Fatalf("runCommand: %v", err) - } - - fmt.Fprintf(os.Stderr, "exit code: %v\n", exitCode) - os.Exit(exitCode) -} - -// SSHAgentConfig holds the config for the SSH Agent -type SSHAgentConfig struct { - userPrivateKeyPath string - userPrivateKeyPassword string - sshSigningServerURL string -} - -func runCommandWithSSHAgent(config *SSHAgentConfig, command string, args []string) (exitCode int, err error) { - agent, err := createSSHAgent(config) - if err != nil { - return 255, fmt.Errorf("failed to setup ssh agent: %v", err) - } - - sshAuthSock, err := sshagent.StartSSHAgentServer(agent) - if err != nil { - return 255, fmt.Errorf("Failed to start ssh agent server: %v", err) - } - fmt.Fprintf(os.Stderr, "Setting SSH_AUTH_SOCK using ssh key: %s\n", config.userPrivateKeyPath) - os.Setenv("SSH_AUTH_SOCK", sshAuthSock) - - // Do string replacement for SSH_AUTH_SOCK - for i, arg := range args { - args[i] = strings.ReplaceAll(arg, "$SSH_AUTH_SOCK", sshAuthSock) - args[i] = strings.ReplaceAll(args[i], "$$SSH_AUTH_SOCK", sshAuthSock) - } - - // Print loaded keys - keyList, err := agent.List() - if err != nil { - return 255, fmt.Errorf("Failed to list sshAgent keys %s: %v", config.userPrivateKeyPath, err) - } - fmt.Fprintf(os.Stderr, "Loaded keys:\n") - for _, key := range keyList { - fmt.Fprintf(os.Stderr, "%s %s\n", strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(key)), "\n"), key.Comment) - } - - return runCommand(command, args) -} - -func createSSHAgent(config *SSHAgentConfig) (sshAgent agent.Agent, err error) { - // TODO: Support mixing keys - if strings.HasPrefix(config.userPrivateKeyPath, "kms://") { - var err error - userPrivateKeyPath := config.userPrivateKeyPath[6:] - - // Setup sshAgent - sshAgent, err = google.NewKMSKeyring(userPrivateKeyPath, config.sshSigningServerURL) - if err != nil { - return nil, fmt.Errorf("Failed to setup KMS Keyring %s: %v", userPrivateKeyPath, err) - } - } else { - // TODO: Create generic keyring that takes array of ssh.Signer's - var privateKey interface{} - privateKeyBytes, err := ioutil.ReadFile(config.userPrivateKeyPath) - if err != nil { - return nil, fmt.Errorf("Failed to read user SSH private key from %s: %v", config.userPrivateKeyPath, err) - } - privateKey, err = sshagent.ParsePrivateSSHKey(privateKeyBytes, config.userPrivateKeyPassword) - if err != nil { - return nil, fmt.Errorf("Failed to read or decrypt SSH private key from %s: %v", config.userPrivateKeyPath, err) - } - sshAgent = agent.NewKeyring() - err = sshAgent.Add(agent.AddedKey{PrivateKey: privateKey, Comment: "my private key"}) - if err != nil { - return nil, err - } - } - return sshAgent, nil -} - -func createSigningServer(caPrivateKeyPath string, sshCaKeyPassword string, address string) (ssh.PublicKey, error) { - var caPublicKey ssh.PublicKey - if strings.HasPrefix(caPrivateKeyPath, "kms://") { - var err error - kmsCaPrivateKeyPath := caPrivateKeyPath[6:] - - // Start the signing server - caPrivateKey, err := google.NewKMSSigner(kmsCaPrivateKeyPath, false) - if err != nil { - return nil, fmt.Errorf("failed google.NewKMSSigner %v", err) - } - caSSHSigner, err := google.NewSSHSignerFromKMSSigner(caPrivateKey) - if err != nil { - return nil, fmt.Errorf("failed google.NewSignerFromSigner from: %v", err) - } - - go func() { - log.Fatal(server.StartHTTPSigningServer(caSSHSigner, address)) - }() - caPublicKey = caPrivateKey.SSHPublicKey() - - } else { - return nil, fmt.Errorf("Not implemented yet") - } - - return caPublicKey, nil -} - -func runCommand(command string, args []string) (exitCode int, err error) { - cmd := exec.Command(command, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Start(); err != nil { - return 1, fmt.Errorf("cmd.Start: %v", err) - } - - err = cmd.Wait() - if err != nil { - if exiterr, ok := err.(*exec.ExitError); ok { - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - return status.ExitStatus(), nil - } - return 1, fmt.Errorf("Failed to get status code: %v", err) - } - return 1, fmt.Errorf("cmd.Wait: %v", err) - } - return 0, nil -} diff --git a/gcemetadata/main.go b/gcemetadata/main.go deleted file mode 100644 index ec24f7b..0000000 --- a/gcemetadata/main.go +++ /dev/null @@ -1,100 +0,0 @@ -package gcemetadata - -import ( - "fmt" - "io" - "log" - "net/http" - "os" - "os/exec" - "strings" -) - -// Links -// * https://github.com/googleapis/google-cloud-go/blob/master/compute/metadata/metadata.go - -// StartMetadateServer start metadata server -func StartMetadateServer(proxyURL string) { - if proxyURL != "" { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // Copy request data and do request - req, err := http.NewRequest(r.Method, proxyURL+r.RequestURI, r.Body) - for name, value := range r.Header { - req.Header.Set(name, value[0]) - } - client := &http.Client{} - resp, err := client.Do(req) - r.Body.Close() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Copy - for k, v := range resp.Header { - w.Header().Set(k, v[0]) - } - w.WriteHeader(resp.StatusCode) - io.Copy(w, resp.Body) - resp.Body.Close() - }) - } else { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(os.Stderr, "req: %s\n", r.RequestURI) - switch r.URL.Path { - case "/computeMetadata/v1/project/project-id": - projectID, err := gcloudGetValue("config", "list", "--format", "value(core.project)") - if err != nil { - log.Fatal(err) - } - metadataResponse(w, http.StatusOK, projectID) - case "/computeMetadata/v1/project/numeric-project-id": - projectID, err := gcloudGetValue("config", "list", "--format", "value(core.project)") - if err != nil { - log.Fatal(err) - } - projectNumber, err := gcloudGetValue("projects", "describe", projectID, "--format", "value(projectNumber)") - if err != nil { - log.Fatal(err) - } - metadataResponse(w, http.StatusOK, projectNumber) - case "/computeMetadata/v1/instance/service-accounts/default/token": - token, err := gcloudGetValue("auth", "print-access-token") - if err != nil { - log.Fatal(err) - } - metadataResponse(w, http.StatusOK, `{"access_token":"`+token+`","expires_in":3487,"token_type":"Bearer"}`) - default: - metadataResponse(w, http.StatusNotFound, `Not found`) - } - }) - } - go func() { - log.Fatal(http.ListenAndServe(":80", nil)) - }() -} - -/* < Metadata-Flavor: Google -< Content-Type: application/text -< ETag: b2830b5c81343278 -< Date: Fri, 22 Nov 2019 20:28:02 GMT -< Server: Metadata Server for VM -< Content-Length: 12 -< X-XSS-Protection: 0 -< X-Frame-Options: SAMEORIGIN -*/ -func metadataResponse(w http.ResponseWriter, statusCode int, message string) { - w.Header().Set("Content-Type", "application/text") - w.Header().Set("Server", "Metadata Server for VM") - w.WriteHeader(statusCode) - fmt.Fprintf(w, "%s", message) -} - -func gcloudGetValue(args ...string) (value string, err error) { - out, err := exec.Command("gcloud", args...).Output() - if err != nil { - return "", err - } - value = strings.TrimSpace(strings.TrimRight(string(out), "\r\n")) - return value, nil -} diff --git a/kms/google/kms-keyring.go b/kms/google/kms-keyring.go deleted file mode 100644 index cfb86e9..0000000 --- a/kms/google/kms-keyring.go +++ /dev/null @@ -1,192 +0,0 @@ -package google - -// TODO: Make generic so it can be used with other key implementation - -import ( - "bytes" - "crypto/rand" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - "time" - - "github.com/connectedcars/auth-wrapper/server" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" -) - -type kmsKeyring struct { - userPrivateKeyPath string - caPrivateKeyPath string - userSSHSigner ssh.Signer - signingServerURL string - signingHTTPClient *http.Client - - locked bool - passphrase []byte -} - -var errLocked = errors.New("agent: locked") - -// NewKMSKeyring returns an Agent that holds keys in memory. It is safe -// for concurrent use by multiple goroutines. -func NewKMSKeyring(userPrivateKeyPath string, signingServerURL string) (sshAgent agent.ExtendedAgent, err error) { - userPrivateKey, err := NewKMSSigner(userPrivateKeyPath, false) - if err != nil { - return nil, err - } - userSSHSigner, err := NewSSHSignerFromKMSSigner(userPrivateKey) - if err != nil { - return nil, fmt.Errorf("failed NewSignerFromSigner from: %v", err) - } - - signingHTTPClient := &http.Client{Timeout: 10 * time.Second} - - return &kmsKeyring{ - userPrivateKeyPath: userPrivateKeyPath, - userSSHSigner: userSSHSigner, - signingHTTPClient: signingHTTPClient, - signingServerURL: signingServerURL, - }, nil -} - -func (r *kmsKeyring) RemoveAll() error { - return fmt.Errorf("removing keys not allowed") -} - -func (r *kmsKeyring) Remove(_ ssh.PublicKey) error { - return fmt.Errorf("removing keys not allowed") -} - -func (r *kmsKeyring) Lock(_ []byte) error { - return fmt.Errorf("locking agent not allowed") -} - -func (r *kmsKeyring) Unlock(_ []byte) error { - return fmt.Errorf("unlocking agent not allowed") -} - -func (r *kmsKeyring) Add(_ agent.AddedKey) error { - return fmt.Errorf("adding new keys not allowed") -} - -// Signers returns signers for all the known keys. -func (r *kmsKeyring) Signers() ([]ssh.Signer, error) { - return nil, fmt.Errorf("Signers not allowed") -} - -// The keyring does not support any extensions -func (r *kmsKeyring) Extension(extensionType string, contents []byte) ([]byte, error) { - return nil, agent.ErrExtensionUnsupported -} - -// List returns the identities known to the agent. -func (r *kmsKeyring) List() ([]*agent.Key, error) { - var ids []*agent.Key - - userPublicKey := r.userSSHSigner.PublicKey() - ids = append(ids, &agent.Key{ - Format: userPublicKey.Type(), - Blob: userPublicKey.Marshal(), - Comment: "user " + r.userPrivateKeyPath}) - - if r.signingServerURL != "" { - // GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } - var challenge server.Challenge - err := r.httpSignRequest("GET", "/certificate/challenge", nil, &challenge) - if err != nil { - return nil, err - } - - // POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } - certRequest := &server.CertificateRequest{ - Challenge: &challenge, - Command: "some command", // TODO: Get command - Args: []string{}, // TODO: Get args - PublicKey: string(ssh.MarshalAuthorizedKey(userPublicKey)), - } - // sign(challenge + command + args) - certRequest.SignRequest(rand.Reader, r.userSSHSigner) - - // get back { certificate: "base64 encoded cert" } - var certResponse server.CertificateResponse - err = r.httpSignRequest("POST", "/certificate", certRequest, &certResponse) - if err != nil { - return nil, err - } - userCertPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certResponse.Certificate)) - if err != nil { - return nil, nil - } - userCert := userCertPubkey.(*ssh.Certificate) - - // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com - // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD - // To fix this we would need to replace the keyname in the certBlob with one of the names listed. - certBlob := userCert.Marshal() - ids = append(ids, &agent.Key{ - Format: userCert.Type(), - Blob: certBlob, - Comment: "user cert " + r.userPrivateKeyPath}) - } - - return ids, nil -} - -// Sign returns a signature for the data. -func (r *kmsKeyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { - return r.SignWithFlags(key, data, 0) -} - -func (r *kmsKeyring) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { - wanted := key.Marshal() - - if bytes.Equal(r.userSSHSigner.PublicKey().Marshal(), wanted) { - // Ignore flags as they google key only supports one type of hashing. - signature, err := r.userSSHSigner.Sign(rand.Reader, data) - if err != nil { - return nil, err - } - return signature, nil - } - - return nil, errors.New("not found") -} - -func (r *kmsKeyring) httpSignRequest(method string, url string, request interface{}, response interface{}) error { - // Convert request to JSON and wrap in io.Reader - var requestBody io.Reader - if request != nil { - jsonBytes, err := json.Marshal(request) - if err != nil { - return err - } - requestBody = bytes.NewReader(jsonBytes) - } - - // Do Request and ready body - challengeRequest, err := http.NewRequest(method, r.signingServerURL+url, requestBody) - if err != nil { - return err - } - challengeResponse, err := r.signingHTTPClient.Do(challengeRequest) - if err != nil { - return err - } - defer challengeResponse.Body.Close() - responseBody, err := ioutil.ReadAll(challengeResponse.Body) - if err != nil { - return err - } - - // Convert JSON to object - err = json.Unmarshal(responseBody, response) - if err != nil { - return err - } - - return nil -} diff --git a/kms/google/kms-signer.go b/kms/google/kms-signer.go index 8dcb507..1da7f40 100644 --- a/kms/google/kms-signer.go +++ b/kms/google/kms-signer.go @@ -14,7 +14,6 @@ import ( "log" cloudkms "cloud.google.com/go/kms/apiv1" - "golang.org/x/crypto/ssh" kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" ) @@ -23,18 +22,16 @@ import ( type KMSSigner interface { crypto.Signer Digest() crypto.Hash - SSHPublicKey() ssh.PublicKey } // kmsSigner is a key type kmsSigner struct { - ctx context.Context - client *cloudkms.KeyManagementClient - keyName string - publicKey crypto.PublicKey - sshPublicKey ssh.PublicKey - digest crypto.Hash - forceDigest bool + ctx context.Context + client *cloudkms.KeyManagementClient + keyName string + publicKey crypto.PublicKey + digest crypto.Hash + forceDigest bool } // CryptoHashLookup maps crypto.hash to string name @@ -80,11 +77,6 @@ func NewKMSSigner(keyName string, forceDigest bool) (signer KMSSigner, err error return nil, fmt.Errorf("x509.ParsePKIXPublicKey: %+v", err) } - sshPublicKey, err := ssh.NewPublicKey(abstractKey) - if err != nil { - return nil, fmt.Errorf("ssh.ParsePublicKey: %+v", err) - } - var publicKey crypto.PublicKey var digestType crypto.Hash switch abstractKey.(type) { @@ -119,13 +111,12 @@ func NewKMSSigner(keyName string, forceDigest bool) (signer KMSSigner, err error } return &kmsSigner{ - keyName: keyName, - ctx: ctx, - client: client, - publicKey: publicKey, - digest: digestType, - sshPublicKey: sshPublicKey, - forceDigest: forceDigest, + keyName: keyName, + ctx: ctx, + client: client, + publicKey: publicKey, + digest: digestType, + forceDigest: forceDigest, }, nil } @@ -177,11 +168,6 @@ func (kmss *kmsSigner) Public() crypto.PublicKey { return kmss.publicKey } -// SSHPublicKey fetches public key in ssh format -func (kmss *kmsSigner) SSHPublicKey() ssh.PublicKey { - return kmss.sshPublicKey -} - // Digest returns hash algo used for this key func (kmss *kmsSigner) Digest() crypto.Hash { return kmss.digest diff --git a/kms/google/kms-ssh-signer.go b/kms/google/kms-ssh-signer.go index 7face86..dd933b9 100644 --- a/kms/google/kms-ssh-signer.go +++ b/kms/google/kms-ssh-signer.go @@ -10,30 +10,30 @@ import ( ) type wrappedSigner struct { - signer KMSSigner - pubKey ssh.PublicKey + signer crypto.Signer + digest crypto.Hash + publicKey ssh.PublicKey } -// NewSSHSignerFromKMSSigner takes a KMSSigner implementation and -// returns a corresponding ssh.Signer interface. -func NewSSHSignerFromKMSSigner(signer KMSSigner) (ssh.Signer, error) { - pubKey, err := ssh.NewPublicKey(signer.Public()) +// NewSSHSignerFromSigner takes a crypto.Signer implementation and returns a corresponding ssh.Signer interface +func NewSSHSignerFromSigner(signer crypto.Signer, digest crypto.Hash) (ssh.AlgorithmSigner, error) { + publicKey, err := ssh.NewPublicKey(signer.Public()) if err != nil { return nil, err } - return &wrappedSigner{signer, pubKey}, nil + return &wrappedSigner{ + signer: signer, + publicKey: publicKey, + digest: digest, + }, nil } func (s *wrappedSigner) PublicKey() ssh.PublicKey { - return s.pubKey + return s.publicKey } func (s *wrappedSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) { - return s.SignWithAlgorithm(rand, data) -} - -func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte) (*ssh.Signature, error) { - hashFunc := s.signer.Digest() + hashFunc := s.digest var digest []byte if hashFunc != 0 { @@ -60,13 +60,11 @@ func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte) (*ssh.Sig algorithm = ssh.SigAlgoRSASHA2512 } } else { - algorithm = s.pubKey.Type() + algorithm = s.publicKey.Type() } - // crypto.Signer.Sign is expected to return an ASN.1-encoded signature - // for ECDSA and DSA, but that's not the encoding expected by SSH, so - // re-encode. - switch s.pubKey.Type() { + // crypto.Signer.Sign is expected to return an ASN.1-encoded signature for ECDSA and DSA, but that's not the encoding expected by SSH, so re-encode. + switch s.publicKey.Type() { case "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "ssh-dss": type asn1Signature struct { R, S *big.Int @@ -77,7 +75,7 @@ func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte) (*ssh.Sig return nil, err } - switch s.pubKey.Type() { + switch s.publicKey.Type() { case "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521": signature = ssh.Marshal(asn1Sig) case "ssh-dss": @@ -94,3 +92,8 @@ func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte) (*ssh.Sig Blob: signature, }, nil } + +func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) { + // Google KSM does not support using other digest algorithms other than what they key was created with so we ignore the algorithm + return s.Sign(rand, data) +} diff --git a/sshagent/keyring.go b/sshagent/keyring.go new file mode 100644 index 0000000..c5545d8 --- /dev/null +++ b/sshagent/keyring.go @@ -0,0 +1,118 @@ +package sshagent + +import ( + "bytes" + "crypto/rand" + "errors" + "fmt" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +// SSHAlgorithmSigner and a comment +type SSHAlgorithmSigner struct { + Signer ssh.AlgorithmSigner + Comment string +} + +// SSHCertificate and a comment +type SSHCertificate struct { + Certificate *ssh.Certificate + Comment string +} + +type sshAlgorithmSignerKeyring struct { + sshAlgorithmSigners *[]SSHAlgorithmSigner + sshCertificates *[]SSHCertificate +} + +// NewSSHAlgorithmSignerKeyring returns an ExtendedAgent +func NewSSHAlgorithmSignerKeyring(sshAlgorithmSigners *[]SSHAlgorithmSigner, sshCertificates *[]SSHCertificate) (agent.ExtendedAgent, error) { + return &sshAlgorithmSignerKeyring{ + sshAlgorithmSigners: sshAlgorithmSigners, + sshCertificates: sshCertificates, + }, nil +} + +func (r *sshAlgorithmSignerKeyring) RemoveAll() error { + return fmt.Errorf("removing keys not allowed") +} + +func (r *sshAlgorithmSignerKeyring) Remove(_ ssh.PublicKey) error { + return fmt.Errorf("removing keys not allowed") +} + +func (r *sshAlgorithmSignerKeyring) Lock(_ []byte) error { + return fmt.Errorf("locking agent not allowed") +} + +func (r *sshAlgorithmSignerKeyring) Unlock(_ []byte) error { + return fmt.Errorf("unlocking agent not allowed") +} + +func (r *sshAlgorithmSignerKeyring) Add(_ agent.AddedKey) error { + return fmt.Errorf("adding new keys not allowed") +} + +func (r *sshAlgorithmSignerKeyring) Signers() ([]ssh.Signer, error) { + return nil, fmt.Errorf("signers not allowed") +} + +// The keyring does not support any extensions +func (r *sshAlgorithmSignerKeyring) Extension(extensionType string, contents []byte) ([]byte, error) { + return nil, agent.ErrExtensionUnsupported +} + +func (r *sshAlgorithmSignerKeyring) List() ([]*agent.Key, error) { + var keys []*agent.Key + + // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com + // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD + // To fix this we would need to replace the keyname in the certBlob with one of the names listed. + for _, certificate := range *r.sshCertificates { + keys = append(keys, &agent.Key{ + Format: certificate.Certificate.Type(), + Blob: certificate.Certificate.Marshal(), + Comment: "cert " + certificate.Comment}) + } + + for _, algorithmSigner := range *r.sshAlgorithmSigners { + keys = append(keys, &agent.Key{ + Format: algorithmSigner.Signer.PublicKey().Type(), + Blob: algorithmSigner.Signer.PublicKey().Marshal(), + Comment: "user " + algorithmSigner.Comment}) + } + + return keys, nil +} + +func (r *sshAlgorithmSignerKeyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + return r.SignWithFlags(key, data, 0) +} + +func (r *sshAlgorithmSignerKeyring) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { + wanted := key.Marshal() + + for _, sshAlgorithmSigner := range *r.sshAlgorithmSigners { + pubKeyBlob := sshAlgorithmSigner.Signer.PublicKey().Marshal() + if bytes.Equal(pubKeyBlob, wanted) { + if flags == 0 { + return sshAlgorithmSigner.Signer.Sign(rand.Reader, data) + } + + var algorithm string + switch flags { + case agent.SignatureFlagRsaSha256: + algorithm = ssh.SigAlgoRSASHA2256 + case agent.SignatureFlagRsaSha512: + algorithm = ssh.SigAlgoRSASHA2512 + default: + return nil, fmt.Errorf("agent: unsupported signature flags: %d", flags) + } + return sshAlgorithmSigner.Signer.SignWithAlgorithm(rand.Reader, data, algorithm) + } + } + + return nil, errors.New("not found") +} diff --git a/sshagent/sshagent-ssh-signer.go b/sshagent/sshagent-ssh-signer.go new file mode 100644 index 0000000..4dcab8c --- /dev/null +++ b/sshagent/sshagent-ssh-signer.go @@ -0,0 +1,43 @@ +package sshagent + +import ( + "fmt" + "io" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +type sshAgentSigner struct { + agent agent.ExtendedAgent + key *agent.Key +} + +// NewSSHAlgorithmSigner returns ssh signer +func NewSSHAlgorithmSigner(agent agent.ExtendedAgent, key *agent.Key) ssh.AlgorithmSigner { + return &sshAgentSigner{ + agent: agent, + key: key, + } +} + +func (s *sshAgentSigner) PublicKey() ssh.PublicKey { + return s.key +} + +func (s *sshAgentSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) { + return s.agent.Sign(s.key, data) +} + +func (s *sshAgentSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) { + var flags agent.SignatureFlags + switch algorithm { + case ssh.SigAlgoRSASHA2256: + flags = agent.SignatureFlagRsaSha256 + case ssh.SigAlgoRSASHA2512: + flags = agent.SignatureFlagRsaSha512 + default: + return nil, fmt.Errorf("unsupported signature algorithm: %s", algorithm) + } + return s.agent.SignWithFlags(s.key, data, flags) +} diff --git a/sshagent/sshagent.go b/sshagent/sshagent.go index ae2d61b..82e151d 100644 --- a/sshagent/sshagent.go +++ b/sshagent/sshagent.go @@ -1,6 +1,7 @@ package sshagent import ( + "fmt" "io/ioutil" "log" "math/rand" @@ -45,6 +46,15 @@ func StartSSHAgentServer(sshAgent agent.Agent) (sshAuthSock string, error error) return sshAuthSock, err } +// ConnectSSHAgent connects to a SSH agent socket and returns a agent.ExtendedAgent +func ConnectSSHAgent(socket string) (agent.ExtendedAgent, error) { + conn, err := net.Dial("unix", string(socket)) + if err != nil { + return nil, fmt.Errorf("net.Dial: %v", err) + } + return agent.NewClient(conn), nil +} + const letterBytes = "abcdefghijklmnopqrstuvwxyz" func generateRandomString(n int) string { From 4ffa32287cbe3461926093f522367859affe2f89 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Fri, 15 May 2020 23:45:23 +0200 Subject: [PATCH 15/32] Clean up cloudbuild --- Dockerfile | 35 ++++++++++++++------ cloudbuild.yaml | 81 ++++++--------------------------------------- localkey/Dockerfile | 11 ------ testdata/Dockerfile | 27 --------------- 4 files changed, 35 insertions(+), 119 deletions(-) delete mode 100644 localkey/Dockerfile delete mode 100644 testdata/Dockerfile diff --git a/Dockerfile b/Dockerfile index 3ef525d..4b1b33d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,23 +20,38 @@ ENV GO111MODULE=on RUN CGO_ENABLED=0 GOOS=linux go build -o auth-wrapper -ldflags "-X 'main.versionString=$VERSION'" ./cmd -# Production image -FROM ${WRAP_IMAGE} as production +# +# Authwrapped git with KMS keys +# +FROM gcr.io/cloud-builders/git as git-kms -ARG WRAP_COMMAND -ARG WRAP_NAME ARG SSH_KEY_PATH COPY --from=builder /app/auth-wrapper /opt/bin/auth-wrapper -RUN ln -s /opt/bin/auth-wrapper /opt/bin/${WRAP_NAME} +RUN ln -s /opt/bin/auth-wrapper /opt/bin/git -# Used by git image ENV GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" -# Force google tools to use the DNS name so we can overwrite it in docker -ENV GCE_METADATA_HOST=metadata.google.internal - ENV PATH=/opt/bin:${PATH} -ENV WRAP_COMMAND=${WRAP_COMMAND} +ENV WRAP_COMMAND=git ENV SSH_KEY_PATH=${SSH_KEY_PATH} ENTRYPOINT ["/opt/bin/auth-wrapper"] + + +# +# Authwrapped git with local keys +# +FROM gcr.io/cloud-builders/git as git-local + +COPY --from=builder /app/auth-wrapper /opt/bin/auth-wrapper +RUN ln -s /opt/bin/auth-wrapper /opt/bin/git + +COPY build.pem / +RUN chmod 600 /build.pem + +ENV GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" + +ENV PATH=/opt/bin:${PATH} +ENV WRAP_COMMAND=git +ENV SSH_KEY_PATH=/build.pem +ENTRYPOINT ["/opt/bin/auth-wrapper"] diff --git a/cloudbuild.yaml b/cloudbuild.yaml index d9fe62a..72f5ea8 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -1,65 +1,17 @@ steps: - # Pull a modern version of docker - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'gcr.io/cloud-builders/docker:latest'] - # Workaround for https://github.com/moby/moby/issues/39120 - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'docker/dockerfile:experimental'] - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'docker/dockerfile:1.0-experimental'] - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'docker.io/docker/dockerfile-copy:v0.1.9'] - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'ubuntu:19.10'] - # Check version - - name: 'gcr.io/cloud-builders/docker' - args: ['version'] - - name: 'gcr.io/cloud-builders/gsutil' - args: ['-c', 'find $$HOME && export'] - entrypoint: "/bin/bash" # # Build KMS auth wrappers # - # TODO: Move to own Dockerfile's, this is getting a bit too fancy # Build auth wrapped git - name: 'gcr.io/cloud-builders/docker' args: [ 'build', - '--build-arg=WRAP_IMAGE=gcr.io/cloud-builders/git', - '--build-arg=WRAP_COMMAND=/usr/bin/git', - '--build-arg=WRAP_NAME=git', + '--target git-kms', '--build-arg=SSH_KEY_PATH=kms://projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3', '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME:$COMMIT_SHA', '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME:latest', '.' ] - # Build auth wrapped docker - - name: 'gcr.io/cloud-builders/docker' - args: [ - 'build', - '--build-arg=WRAP_IMAGE=gcr.io/cloud-builders/docker', - '--build-arg=WRAP_COMMAND=/usr/bin/docker', - '--build-arg=WRAP_NAME=docker', - '--build-arg=SSH_KEY_PATH=kms://projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3', - '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-docker.$BRANCH_NAME:$COMMIT_SHA', - '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-docker.$BRANCH_NAME:latest', - '.' - ] - # Test auth wrapped docker using KSM key - - name: 'gcr.io/$PROJECT_ID/$REPO_NAME-docker.$BRANCH_NAME:$COMMIT_SHA' - args: [ - 'build', - '--no-cache', - '--progress=plain', - '--ssh=default=$$SSH_AUTH_SOCK', - #'--network=host', - #'--add-host=metadata.google.internal:127.0.0.1', - '.' - ] - dir: 'testdata' - env: - - "PROGRESS_NO_TRUNC=1" - - "DOCKER_BUILDKIT=1" # Test auth wrapped git using KSM key - name: 'gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME:$COMMIT_SHA' args: ['clone', 'git@github.com:connectedcars/private-module.git'] @@ -71,40 +23,27 @@ steps: args: [ 'cp', 'gs://connectedcars-staging-cloudbuilder-private/build.pem', - './localkey' + './build.pem' ] - # Build auth wrapper docker image + # Build auth wrapper git image - name: 'gcr.io/cloud-builders/docker' dir: localkey args: [ 'build', - '--build-arg=FROM_IMAGE=gcr.io/$PROJECT_ID/$REPO_NAME-docker.$BRANCH_NAME:$COMMIT_SHA', - '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-docker-pemkey.$BRANCH_NAME:$COMMIT_SHA', - '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-docker-pemkey.$BRANCH_NAME:latest', '.' + '--target git-local', + '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-git-local.$BRANCH_NAME:$COMMIT_SHA', + '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-git-local.$BRANCH_NAME:latest', '.' ] - # Test cloud build wrapper using ssh key embedded in the container - - name: 'gcr.io/$PROJECT_ID/$REPO_NAME-docker-pemkey.$BRANCH_NAME:$COMMIT_SHA' - args: [ - 'build', - '--no-cache', - '--progress=plain', - '--ssh=default=$$SSH_AUTH_SOCK', - #'--network=host', - #'--add-host=metadata.google.internal:127.0.0.1', - '.' - ] - dir: 'testdata' + # Test auth wrapped git using local key + - name: 'gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME:$COMMIT_SHA' + args: ['clone', 'git@github.com:connectedcars/private-module.git'] secretEnv: - 'SSH_KEY_PASSWORD' - env: - - "PROGRESS_NO_TRUNC=1" - - "DOCKER_BUILDKIT=1" secrets: - kmsKeyName: projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/connectedcars-builder secretEnv: SSH_KEY_PASSWORD: CiQAg7wCPfO2Tf9mtZoFWjAtX7whQ481af3gyGdM9WNK26B74UkSUQBefMgeHNh0KTsGybKReXDsFcbmed7f5sw97zSe9cswpKogENM5Ye0jiIu6NfebUpCnmJ9HVHmD/yBknlW4nn1VXBs7HYGiBSFZ52i2HyEopw== images: [ 'gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME', - 'gcr.io/$PROJECT_ID/$REPO_NAME-docker.$BRANCH_NAME', - 'gcr.io/$PROJECT_ID/$REPO_NAME-docker-pemkey.$BRANCH_NAME' + 'gcr.io/$PROJECT_ID/$REPO_NAME-git-local.$BRANCH_NAME' ] diff --git a/localkey/Dockerfile b/localkey/Dockerfile deleted file mode 100644 index 4b49168..0000000 --- a/localkey/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -ARG FROM_IMAGE -FROM ${FROM_IMAGE} as production - -# Force google tools to use the DNS name so we can overwrite it in docker -ENV GCE_METADATA_HOST=metadata.google.internal - -ARG SSH_KEY_PATH=/build.pem -ENV SSH_KEY_PATH=${SSH_KEY_PATH} - -COPY build.pem / -RUN chmod 600 /build.pem diff --git a/testdata/Dockerfile b/testdata/Dockerfile deleted file mode 100644 index a7584b4..0000000 --- a/testdata/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# syntax = docker/dockerfile:experimental -FROM ubuntu:19.10 as builder - -# Force go tools to use the DNS name so we can overwrite it in docker -ENV GCE_METADATA_HOST=metadata.google.internal -# Used by google python code -#ENV GCE_METADATA_ROOT=metadata.google.internal - -# Test ssh key injection -RUN apt-get update -qq && \ - apt-get dist-upgrade -qq -y --no-install-recommends && \ - apt-get install -qq -y --no-install-recommends git openssh-client && \ - # curl ca-certificates python python3 && \ - rm -rf /var/lib/apt/lists/* - -# Install gcloud sdk -# RUN curl -sSL https://sdk.cloud.google.com | bash -# ENV PATH $PATH:/root/google-cloud-sdk/bin -# RUN curl -s -v -H "Accept-Encoding: identity" -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/project/numeric-project-id -# RUN gcloud auth print-access-token -# RUN gsutil -DD ls gs://connectedcars-staging-cloudbuilder-private/ - -RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts - -ENV GIT_SSH_COMMAND="ssh -vvvv" - -RUN --mount=type=ssh,required=true export && git clone git@github.com:connectedcars/private-module.git From c2ec18ff2f9075b6cddccf9623d7e261e23ff131 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Fri, 15 May 2020 23:50:12 +0200 Subject: [PATCH 16/32] Fix docker cmd --- cloudbuild.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 72f5ea8..d4d8860 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -6,7 +6,7 @@ steps: - name: 'gcr.io/cloud-builders/docker' args: [ 'build', - '--target git-kms', + '--target=git-kms', '--build-arg=SSH_KEY_PATH=kms://projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/3', '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME:$COMMIT_SHA', '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME:latest', @@ -30,7 +30,7 @@ steps: dir: localkey args: [ 'build', - '--target git-local', + '--target=git-local', '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-git-local.$BRANCH_NAME:$COMMIT_SHA', '--tag=gcr.io/$PROJECT_ID/$REPO_NAME-git-local.$BRANCH_NAME:latest', '.' ] From 8e742202bb4a1d95638e658d23ee5c3750381e70 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Fri, 15 May 2020 23:52:47 +0200 Subject: [PATCH 17/32] Fix name in build --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4b1b33d..7a771eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN go version ENV GO111MODULE=on -RUN CGO_ENABLED=0 GOOS=linux go build -o auth-wrapper -ldflags "-X 'main.versionString=$VERSION'" ./cmd +RUN CGO_ENABLED=0 GOOS=linux go build -o auth-wrapper -ldflags "-X 'main.versionString=$VERSION'" ./cmd/authwrapper # # Authwrapped git with KMS keys From 353a657fb9a4dfa95d2ebb0d53a1d5fa92bc5c09 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Fri, 15 May 2020 23:57:38 +0200 Subject: [PATCH 18/32] Fix dir --- cloudbuild.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index d4d8860..ebbf12c 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -27,7 +27,6 @@ steps: ] # Build auth wrapper git image - name: 'gcr.io/cloud-builders/docker' - dir: localkey args: [ 'build', '--target=git-local', From 4fd5b8eafd35dd1af74ed158efe7aaa99fcee763 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Sat, 16 May 2020 00:05:02 +0200 Subject: [PATCH 19/32] Add cleanup --- cloudbuild.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index ebbf12c..3c11947 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -15,6 +15,9 @@ steps: # Test auth wrapped git using KSM key - name: 'gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME:$COMMIT_SHA' args: ['clone', 'git@github.com:connectedcars/private-module.git'] + - name: 'gcr.io/cloud-builders/git' + entrypoint: 'bash' + args: ['-c', 'rm -rf private-module'] # # Build embedded key auth wrappers # @@ -38,6 +41,9 @@ steps: args: ['clone', 'git@github.com:connectedcars/private-module.git'] secretEnv: - 'SSH_KEY_PASSWORD' + - name: 'gcr.io/cloud-builders/git' + entrypoint: 'bash' + args: ['-c', 'rm -rf private-module'] secrets: - kmsKeyName: projects/connectedcars-staging/locations/global/keyRings/cloudbuilder/cryptoKeys/connectedcars-builder secretEnv: From 10665027bd33b7d0bf3d455241c7b0afc73c98cb Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Sun, 17 May 2020 19:43:16 +0200 Subject: [PATCH 20/32] Start limiting the user cert issued --- cmd/authwrapper/main.go | 77 +++++--------------- cmd/authwrapper/setup.go | 150 +++++++++++++++++++++++++++------------ cmd/authwrapper/utils.go | 54 ++++++++++---- server/http.go | 45 ++++++++++-- server/main.go | 65 +++++++++-------- server/utils.go | 116 ++++++++++++++++++++++++++++++ sshagent/keyring.go | 12 ++-- 7 files changed, 364 insertions(+), 155 deletions(-) diff --git a/cmd/authwrapper/main.go b/cmd/authwrapper/main.go index 6d2f217..07cd8be 100644 --- a/cmd/authwrapper/main.go +++ b/cmd/authwrapper/main.go @@ -4,90 +4,51 @@ import ( "fmt" "log" "os" - "path/filepath" "strings" "golang.org/x/crypto/ssh" ) func main() { - processName := filepath.Base(os.Args[0]) - config, err := parseEnvironment() if err != nil { log.Fatalf(": %v", err) } - var command string - var args []string - if config.WrapCommand != "" { - command = config.WrapCommand - args = os.Args[1:] - } else if processName != "auth-wrapper" && processName != "__debug_bin" { - // Get executable path - ex, err := os.Executable() - if err != nil { - panic(err) - } - processPath := filepath.Dir(ex) - - // Remove wrapper location path - currentPath := os.Getenv("PATH") - cleanedPath := strings.Replace(currentPath, processPath+"/:", "", 1) - cleanedPath = strings.Replace(cleanedPath, processPath+":", "", 1) - os.Setenv("PATH", cleanedPath) - - command = processName - args = os.Args[1:] - } else { - if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "auth-wrapper cmd args") - os.Exit(1) - } - // Setup exec command - command = os.Args[1] - args = os.Args[2:] - } - + // TODO: Default to port if nothing has been set if config.SSHCaKeyPath != "" && config.SSHSigningServerAddress != "" { caPublickey, err := startSigningServer( config.SSHCaKeyPath, config.SSHCaKeyPassword, + config.SSHCaAuthorizedKeysPath, config.SSHSigningServerAddress, ) if err != nil { log.Fatalf("createSigningServer: %v", err) } pubkeyString := strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(caPublickey)), "\n") + fmt.Fprintf(os.Stderr, "Starting signing server on %s with key:", config.SSHSigningServerAddress) fmt.Fprintf(os.Stderr, "%s %s\n", pubkeyString, "ca "+config.SSHCaKeyPath) } - var exitCode int - if config.SSHKeyPath != "" || config.SSHAgentSocket != "" { - agent, err := setupKeyring(config) - if err != nil { - log.Fatalf("Failed to setup keyring: %v", err) - } + agent, err := setupKeyring(config) + if err != nil { + log.Fatalf("Failed to setup keyring: %v", err) + } - // List loaded keys - keyList, err := agent.List() - if err != nil { - log.Fatalf("Failed to list sshAgent keys: %v", err) - } - fmt.Fprintf(os.Stderr, "Loaded keys:\n") - for _, key := range keyList { - fmt.Fprintf(os.Stderr, "%s %s\n", strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(key)), "\n"), key.Comment) - } + // List loaded keys + keyList, err := agent.List() + if err != nil { + log.Fatalf("Failed to list sshAgent keys: %v", err) + } + fmt.Fprintf(os.Stderr, "Loaded keys:\n") + for _, key := range keyList { + fmt.Fprintf(os.Stderr, "%s %s\n", strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(key)), "\n"), key.Comment) + } - exitCode, err = runCommandWithSSHAgent(agent, command, args) - if err != nil { - log.Fatalf("runCommandWithSSHAgent: %v", err) - } - } else { - exitCode, err = runCommand(command, args) - if err != nil { - log.Fatalf("runCommand: %v", err) - } + exitCode, err := runCommandWithSSHAgent(agent, config.Command, config.Args) + if err != nil { + log.Fatalf("runCommandWithSSHAgent: %v", err) } fmt.Fprintf(os.Stderr, "exit code: %v\n", exitCode) diff --git a/cmd/authwrapper/setup.go b/cmd/authwrapper/setup.go index 43e7534..8683bbb 100644 --- a/cmd/authwrapper/setup.go +++ b/cmd/authwrapper/setup.go @@ -2,9 +2,11 @@ package main import ( "crypto/rand" + "flag" "fmt" "io/ioutil" "os" + "path/filepath" "strings" "github.com/connectedcars/auth-wrapper/kms/google" @@ -16,24 +18,34 @@ import ( // Config contains the auth wrapper configuration type Config struct { - WrapCommand string + Command string + Args []string + RequestedPrincipals []string SSHKeyPath string SSHKeyPassword string SSHSigningServerURL string SSHCaKeyPath string SSHCaKeyPassword string + SSHCaAuthorizedKeysPath string SSHSigningServerAddress string SSHAgentSocket string } +var principalsFlag = flag.String("principals", "", "requested principals") + func parseEnvironment() (*Config, error) { + flag.Parse() + config := &Config{ - WrapCommand: os.Getenv("WRAP_COMMAND"), + Command: os.Getenv("WRAP_COMMAND"), + Args: os.Args[1:], + RequestedPrincipals: strings.Split(os.Getenv("PRINCIPALS"), ","), SSHKeyPath: os.Getenv("SSH_KEY_PATH"), SSHKeyPassword: os.Getenv("SSH_KEY_PASSWORD"), SSHSigningServerURL: os.Getenv("SSH_SIGNING_SERVER_URL"), SSHCaKeyPath: os.Getenv("SSH_CA_KEY_PATH"), SSHCaKeyPassword: os.Getenv("SSH_CA_KEY_PASSWORD"), + SSHCaAuthorizedKeysPath: os.Getenv("SSH_CA_AUTHORIZED_KEYS_PATH"), SSHSigningServerAddress: os.Getenv("SSH_SIGNING_SERVER_LISTEN_ADDRESS"), SSHAgentSocket: os.Getenv("SSH_AUTH_SOCK"), } @@ -46,7 +58,41 @@ func parseEnvironment() (*Config, error) { os.Unsetenv("SSH_SIGNING_SERVER_LISTEN_ADDRESS") os.Unsetenv("SSH_AUTH_SOCK") - // TODO: Do basic error validation + if *principalsFlag != "" { + config.RequestedPrincipals = strings.Split(*principalsFlag, ",") + } + + if config.Command == "" { + processName := filepath.Base(os.Args[0]) + if processName != "auth-wrapper" && processName != "__debug_bin" { + // Get executable path + ex, err := os.Executable() + if err != nil { + panic(err) + } + processPath := filepath.Dir(ex) + + // Remove wrapper location path + currentPath := os.Getenv("PATH") + cleanedPath := strings.Replace(currentPath, processPath+"/:", "", 1) + cleanedPath = strings.Replace(cleanedPath, processPath+":", "", 1) + os.Setenv("PATH", cleanedPath) + + config.Command = processName + } else { + if len(os.Args) < 2 { + return nil, fmt.Errorf("auth-wrapper cmd args") + } + config.Command = os.Args[1] + config.Args = os.Args[2:] + } + } + + // TODO: Do more config error validation + + if config.SSHSigningServerURL != "" && len(config.RequestedPrincipals) == 0 { + return nil, fmt.Errorf("When SSH_SIGNING_SERVER_URL is set a list of principals needs to be provided") + } return config, nil } @@ -56,7 +102,6 @@ func setupKeyring(config *Config) (agent.ExtendedAgent, error) { var certificates []sshagent.SSHCertificate if config.SSHKeyPath != "" { - var userSigner ssh.AlgorithmSigner if strings.HasPrefix(config.SSHKeyPath, "kms://") { var err error userPrivateKeyPath := config.SSHKeyPath[6:] @@ -70,9 +115,8 @@ func setupKeyring(config *Config) (agent.ExtendedAgent, error) { } signers = append(signers, sshagent.SSHAlgorithmSigner{ Signer: signer, - Comment: "google kms key " + userPrivateKeyPath, + Comment: config.SSHKeyPath, }) - userSigner = signer } else { privateKeyBytes, err := ioutil.ReadFile(config.SSHKeyPath) if err != nil { @@ -92,47 +136,9 @@ func setupKeyring(config *Config) (agent.ExtendedAgent, error) { } signers = append(signers, sshagent.SSHAlgorithmSigner{ Signer: algorithmSigner, - Comment: "local key " + config.SSHKeyPath, - }) - userSigner = algorithmSigner - } - - if config.SSHSigningServerURL != "" { - // GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } - var challenge server.Challenge - err := httpJSONRequest("GET", config.SSHSigningServerURL+"/certificate/challenge", nil, &challenge) - if err != nil { - return nil, err - } - - // POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } - certRequest := &server.CertificateRequest{ - Challenge: &challenge, - Command: "some command", // TODO: Get command - Args: []string{}, // TODO: Get args - PublicKey: string(ssh.MarshalAuthorizedKey(userSigner.PublicKey())), - } - // sign(challenge + command + args) - certRequest.SignRequest(rand.Reader, userSigner) - - // get back { certificate: "base64 encoded cert" } - var certResponse server.CertificateResponse - err = httpJSONRequest("POST", config.SSHSigningServerURL+"/certificate", certRequest, &certResponse) - if err != nil { - return nil, err - } - userCertPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certResponse.Certificate)) - if err != nil { - return nil, nil - } - userCert := userCertPubkey.(*ssh.Certificate) - - certificates = append(certificates, sshagent.SSHCertificate{ - Certificate: userCert, - Comment: "user key " + config.SSHKeyPath, + Comment: config.SSHKeyPath, }) } - } if config.SSHAgentSocket != "" { @@ -150,10 +156,60 @@ func setupKeyring(config *Config) (agent.ExtendedAgent, error) { signer := sshagent.NewSSHAlgorithmSigner(agent, key) signers = append(signers, sshagent.SSHAlgorithmSigner{ Signer: signer, - Comment: "agent key", + Comment: "agent key " + key.Comment, }) } } - return sshagent.NewSSHAlgorithmSignerKeyring(&signers, &certificates) + if config.SSHSigningServerURL != "" { + // TODO: Do something with the errors + var errors []error + for _, signer := range signers { + userCert, err := fetchUserCert(config.SSHSigningServerURL, signer.Signer, config.Command, config.Args, config.RequestedPrincipals) + if err != nil { + errors = append(errors, err) + continue + } + certificates = append(certificates, sshagent.SSHCertificate{ + Certificate: userCert, + Comment: "key " + config.SSHKeyPath, + }) + } + } + + return sshagent.NewSSHAlgorithmSignerKeyring(signers, certificates) +} + +func fetchUserCert(signingServerURL string, signer ssh.AlgorithmSigner, command string, args []string, principals []string) (*ssh.Certificate, error) { + // GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } + var challenge server.Challenge + err := httpJSONRequest("GET", signingServerURL+"/certificate/challenge", nil, &challenge) + if err != nil { + return nil, err + } + + // POST /certificate # { challenge: "\...value", command: "", args: "", pubkey: "..." signature: "signed by user key" } + certRequest := &server.CertificateRequest{ + Challenge: &challenge, + Command: command, + Args: args, + Principals: principals, + PublicKey: strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(signer.PublicKey())), "\n"), + } + + // sign(challenge + command + args) + certRequest.SignRequest(rand.Reader, signer) + + // get back { certificate: "base64 encoded cert" } + var certResponse server.CertificateResponse + err = httpJSONRequest("POST", signingServerURL+"/certificate", certRequest, &certResponse) + if err != nil { + return nil, err + } + userCertPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certResponse.Certificate)) + if err != nil { + return nil, nil + } + userCert := userCertPubkey.(*ssh.Certificate) + return userCert, nil } diff --git a/cmd/authwrapper/utils.go b/cmd/authwrapper/utils.go index 54ced79..4154c3c 100644 --- a/cmd/authwrapper/utils.go +++ b/cmd/authwrapper/utils.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "bytes" "encoding/json" "fmt" @@ -39,7 +40,7 @@ func runCommandWithSSHAgent(agent agent.ExtendedAgent, command string, args []st return runCommand(command, args) } -func startSigningServer(caPrivateKeyPath string, sshCaKeyPassword string, address string) (ssh.PublicKey, error) { +func startSigningServer(caPrivateKeyPath string, keyPassword string, authorizedKeysPath, address string) (ssh.PublicKey, error) { var caPublicKey ssh.PublicKey if strings.HasPrefix(caPrivateKeyPath, "kms://") { var err error @@ -52,11 +53,21 @@ func startSigningServer(caPrivateKeyPath string, sshCaKeyPassword string, addres } caSSHSigner, err := google.NewSSHSignerFromSigner(caPrivateKey, caPrivateKey.Digest()) if err != nil { - return nil, fmt.Errorf("failed google.NewSignerFromSigner from: %v", err) + return nil, fmt.Errorf("failed google.NewSSHSignerFromSigner: %v", err) + } + + authorizedKeysLines, err := readLines(authorizedKeysPath) + if err != nil { + return nil, fmt.Errorf("failed readLines: %v", err) + } + + allowedKeys, err := server.ParseAuthorizedKeys(authorizedKeysLines) + if err != nil { + return nil, fmt.Errorf("failed parse ParseAuthorizedKeys: %v", err) } go func() { - log.Fatal(server.StartHTTPSigningServer(caSSHSigner, address)) + log.Fatal(server.StartHTTPSigningServer(caSSHSigner, allowedKeys, address)) }() caPublicKey = caSSHSigner.PublicKey() @@ -90,11 +101,11 @@ func runCommand(command string, args []string) (exitCode int, err error) { return 0, nil } -func httpJSONRequest(method string, url string, request interface{}, response interface{}) error { +func httpJSONRequest(method string, url string, requestData interface{}, responseData interface{}) error { // Convert request to JSON and wrap in io.Reader var requestBody io.Reader - if request != nil { - jsonBytes, err := json.Marshal(request) + if requestData != nil { + jsonBytes, err := json.Marshal(requestData) if err != nil { return err } @@ -102,25 +113,44 @@ func httpJSONRequest(method string, url string, request interface{}, response in } // Do Request and ready body - challengeRequest, err := http.NewRequest(method, url, requestBody) + httpRequest, err := http.NewRequest(method, url, requestBody) if err != nil { return err } - challengeResponse, err := httpClient.Do(challengeRequest) + httpResponse, err := httpClient.Do(httpRequest) if err != nil { return err } - defer challengeResponse.Body.Close() - responseBody, err := ioutil.ReadAll(challengeResponse.Body) + defer httpResponse.Body.Close() + responseBody, err := ioutil.ReadAll(httpResponse.Body) if err != nil { return err } + if httpResponse.StatusCode != 200 { + return fmt.Errorf("%s %s failed(%d): %s", method, url, httpResponse.StatusCode, responseBody) + } + // Convert JSON to object - err = json.Unmarshal(responseBody, response) + err = json.Unmarshal(responseBody, responseData) if err != nil { - return err + return fmt.Errorf("failed to parse JSON in '%s': %v", responseBody, err) } return nil } + +func readLines(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines, scanner.Err() +} diff --git a/server/http.go b/server/http.go index 8f61081..42c3e1f 100644 --- a/server/http.go +++ b/server/http.go @@ -2,8 +2,10 @@ package server import ( "encoding/json" + "fmt" "io/ioutil" "net/http" + "path/filepath" "golang.org/x/crypto/ssh" ) @@ -20,8 +22,9 @@ type HTTPSigningServer struct { } // StartHTTPSigningServer returns a HTTPSigningServer -func StartHTTPSigningServer(caKey ssh.Signer, listenAddr string) error { - signingServer := NewSigningServer(caKey) +func StartHTTPSigningServer(caKey ssh.Signer, allowedKeys []AllowedKey, listenAddr string) error { + signingServer := NewSigningServer(caKey, allowedKeys) + httpSigningServer := &HTTPSigningServer{signingServer: signingServer} http.Handle("/", httpSigningServer) @@ -80,12 +83,46 @@ func (s *HTTPSigningServer) postCertificate(w http.ResponseWriter, r *http.Reque return nil, &StatusError{400, err} } - userPublickey, err := s.signingServer.VerifyCertificateRequest(&certRequest) + // Validate input + if certRequest.Challenge == nil { + return nil, &StatusError{400, fmt.Errorf("challenge not set")} + } + if certRequest.Principals == nil { + return nil, &StatusError{400, fmt.Errorf("principals not set")} + } + if certRequest.Signature == nil { + return nil, &StatusError{400, fmt.Errorf("signature not set")} + } + + // Check if this request is allowed + allowedKey, err := s.signingServer.VerifyCertificateRequest(&certRequest) if err != nil { return nil, &StatusError{400, err} } + if allowedKey == nil { + return nil, &StatusError{401, fmt.Errorf("Key not allowed")} + } + + // Check requested principals are allowed + for _, requestedPrincipal := range certRequest.Principals { + for i, allowedPrincipal := range allowedKey.Principals { + match, err := filepath.Match(allowedPrincipal, requestedPrincipal) + if err != nil { + return nil, &StatusError{500, fmt.Errorf("allowed pattern is malformed")} + } + if match { + break + } + if i == len(allowedKey.Principals)-1 { + return nil, &StatusError{400, fmt.Errorf("requested principal '%s' not allowed", requestedPrincipal)} + } + } + } + + // TODO: Check if command is allowed https://github.com/tlbdk/socketauth/blob/master/src/sshutils.test.js + // TODO: Add SSH command to Options so we are sure only that command will be run - userCert, err := s.signingServer.IssueUserCertificate(userPublickey) + userCert, err := s.signingServer.IssueUserCertificate(allowedKey, certRequest.Principals) if err != nil { return nil, &StatusError{500, err} } diff --git a/server/main.go b/server/main.go index 00853a8..1d7874a 100644 --- a/server/main.go +++ b/server/main.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "strconv" "strings" "time" @@ -25,11 +26,12 @@ type challengeValue struct { // CertificateRequest for SSH user certificate type CertificateRequest struct { - Challenge *Challenge `json:"challenge"` - Command string `json:"command"` - Args []string `json:"args"` - PublicKey string `json:"publicKey"` - Signature *ssh.Signature `json:"signature"` + Challenge *Challenge `json:"challenge"` + Principals []string `json:"principals"` + Command string `json:"command"` + Args []string `json:"args"` + PublicKey string `json:"publicKey"` + Signature *ssh.Signature `json:"signature"` } // SignRequest signs request with provided user key : Move to common lib as this is used by the client @@ -50,23 +52,29 @@ type CertificateResponse struct { // SigningServer struct type SigningServer struct { - caKey ssh.Signer + caKey ssh.Signer + allowedKeysMap map[string]*AllowedKey } // NewSigningServer creates a new server -func NewSigningServer(caKey ssh.Signer) *SigningServer { - return &SigningServer{caKey: caKey} +func NewSigningServer(caKey ssh.Signer, allowedKeys []AllowedKey) *SigningServer { + var allowedKeysMap = map[string]*AllowedKey{} + for i, allowedKey := range allowedKeys { + pubkeyString := strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(allowedKey.Key)), "\n") + allowedKeysMap[pubkeyString] = &allowedKeys[i] + } + return &SigningServer{ + caKey: caKey, + allowedKeysMap: allowedKeysMap, + } } // VerifyCertificateRequest errors if it fails validation -func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest) (pubkey ssh.PublicKey, err error) { +func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest) (*AllowedKey, error) { // Validate challenge came from us challenge := certRequest.Challenge - if challenge == nil { - return nil, fmt.Errorf("Challenge not set") - } - err = s.caKey.PublicKey().Verify(challenge.Value, challenge.Signature) + err := s.caKey.PublicKey().Verify(challenge.Value, challenge.Signature) if err != nil { return nil, err } @@ -77,37 +85,37 @@ func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest if err != nil { return nil, err } - // TODO: Check if challenge expired - // TODO: Look up public key instead of parsing it - userPubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certRequest.PublicKey)) - if err != nil { - return nil, err + + allowedKey := s.allowedKeysMap[certRequest.PublicKey] + if allowedKey == nil { + return nil, nil } + // TODO: Check if allowedKey is expired? payload := GenerateSigningPayload(certRequest) // Verify that public key signed it - err = userPubkey.Verify(payload, certRequest.Signature) + err = allowedKey.Key.Verify(payload, certRequest.Signature) if err != nil { return nil, err } - return userPubkey, nil + return allowedKey, nil } // IssueUserCertificate issues ssh user certificate -func (s *SigningServer) IssueUserCertificate(userPublicKey ssh.PublicKey) (userCertificate *ssh.Certificate, err error) { +func (s *SigningServer) IssueUserCertificate(allowedKey *AllowedKey, principals []string) (userCertificate *ssh.Certificate, err error) { userCert := &ssh.Certificate{ - Key: userPublicKey, - KeyId: "test", + Key: allowedKey.Key, + KeyId: strconv.Itoa(allowedKey.Index), CertType: ssh.UserCert, - ValidPrincipals: []string{"tlb"}, + ValidPrincipals: principals, ValidAfter: 0, - ValidBefore: ssh.CertTimeInfinity, // uint64(time.Now().Add(time.Minute * 60).Unix()), + ValidBefore: allowedKey.ValidBefore, Permissions: ssh.Permissions{ - CriticalOptions: map[string]string{}, - Extensions: map[string]string{}, + CriticalOptions: allowedKey.Options, + Extensions: allowedKey.Extensions, }, } @@ -127,8 +135,9 @@ func GenerateSigningPayload(certRequest *CertificateRequest) (payload []byte) { payload = challenge.Value payload = append(payload, challenge.Signature.Format...) payload = append(payload, challenge.Signature.Blob...) + payload = append(payload, strings.Join(certRequest.Principals, ",")...) payload = append(payload, certRequest.Command...) - payload = append(payload, strings.Join(certRequest.Args, "")...) + payload = append(payload, strings.Join(certRequest.Args, ",")...) return payload } diff --git a/server/utils.go b/server/utils.go index 04459f4..9890ee6 100644 --- a/server/utils.go +++ b/server/utils.go @@ -2,6 +2,10 @@ package server import ( "crypto/rand" + "fmt" + "strings" + + "golang.org/x/crypto/ssh" ) // GenerateRamdomBytes from cryptographically secure source @@ -13,3 +17,115 @@ func GenerateRamdomBytes(length int) (value []byte, err error) { } return randomBytes, nil } + +// AllowedKey contains the allowed values for this key +type AllowedKey struct { + Index int + Key ssh.PublicKey + Comment string + Principals []string + ValidBefore uint64 + Options map[string]string + Extensions map[string]string +} + +// ParseAuthorizedKeys to []AllowedCertKey format +func ParseAuthorizedKeys(lines []string) ([]AllowedKey, error) { + keys := []AllowedKey{} + + // http://man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT + // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys + for i, line := range lines { + publicKey, comment, options, _, err := ssh.ParseAuthorizedKey([]byte(line)) + if err != nil { + return nil, fmt.Errorf("failed to parse line '%s': %v", line, err) + } + + key := AllowedKey{ + Index: i, + Key: publicKey, + ValidBefore: ssh.CertTimeInfinity, // TODO: Set a better value // uint64(time.Now().Add(time.Minute * 60).Unix()), + Comment: comment, + Principals: []string{}, + Options: map[string]string{}, + Extensions: map[string]string{}, + } + + restricted := false + disallowedExtensions := []string{""} + for _, option := range options { + nameValue := strings.Split(option, "=") + name := nameValue[0] + var value string + if len(nameValue) > 1 { + value = trimQuotes(nameValue[1]) + } + switch name { + case "agent-forwarding": + key.Extensions["permit-agent-forwarding"] = value + case "command": + // TODO: Don't allow empty commands + key.Options["force-command"] = value + case "expiry-time": + // TODO: Use this to ignore key after date + case "from": + // TODO: Convert wildcard matching to CIDR address/masklen notation + key.Options["source-address"] = value + case "no-agent-forwarding": + disallowedExtensions = append(disallowedExtensions, "permit-agent-forwarding") + case "no-port-forwarding": + disallowedExtensions = append(disallowedExtensions, "permit-port-forwarding") + case "no-pty": + disallowedExtensions = append(disallowedExtensions, "permit-pty") + case "no-user-rc": + disallowedExtensions = append(disallowedExtensions, "permit-user-rc") + case "no-X11-forwarding": + disallowedExtensions = append(disallowedExtensions, "permit-X11-forwarding") + case "port-forwarding": + key.Extensions["permit-port-forwarding"] = value + case "principals": + key.Principals = strings.Split(value, ",") + case "pty": + key.Extensions["permit-pty"] = value + case "no-touch-required": + key.Extensions["no-presence-required"] = value + case "restrict": + restricted = true + case "user-rc": + key.Extensions["permit-user-rc"] = value + case "X11-forwarding": + key.Extensions["permit-X11-forwarding"] = value + default: + return nil, fmt.Errorf("unknown option %s", name) + } + } + + // Enable all extensions if they are not restricted + if !restricted { + key.Extensions["no-presence-required"] = "" + key.Extensions["permit-X11-forwarding"] = "" + key.Extensions["permit-agent-forwarding"] = "" + key.Extensions["permit-port-forwarding"] = "" + key.Extensions["permit-pty"] = "" + key.Extensions["permit-user-rc"] = "" + } + + // Remove all extentions that have been explicitly forbidden + for _, extension := range disallowedExtensions { + delete(key.Extensions, extension) + } + + keys = append(keys, key) + } + + return keys, nil +} + +func trimQuotes(s string) string { + if len(s) >= 2 { + if s[0] == '"' && s[len(s)-1] == '"' { + return s[1 : len(s)-1] + } + } + return s +} diff --git a/sshagent/keyring.go b/sshagent/keyring.go index c5545d8..5c4dae3 100644 --- a/sshagent/keyring.go +++ b/sshagent/keyring.go @@ -23,12 +23,12 @@ type SSHCertificate struct { } type sshAlgorithmSignerKeyring struct { - sshAlgorithmSigners *[]SSHAlgorithmSigner - sshCertificates *[]SSHCertificate + sshAlgorithmSigners []SSHAlgorithmSigner + sshCertificates []SSHCertificate } // NewSSHAlgorithmSignerKeyring returns an ExtendedAgent -func NewSSHAlgorithmSignerKeyring(sshAlgorithmSigners *[]SSHAlgorithmSigner, sshCertificates *[]SSHCertificate) (agent.ExtendedAgent, error) { +func NewSSHAlgorithmSignerKeyring(sshAlgorithmSigners []SSHAlgorithmSigner, sshCertificates []SSHCertificate) (agent.ExtendedAgent, error) { return &sshAlgorithmSignerKeyring{ sshAlgorithmSigners: sshAlgorithmSigners, sshCertificates: sshCertificates, @@ -70,14 +70,14 @@ func (r *sshAlgorithmSignerKeyring) List() ([]*agent.Key, error) { // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD // To fix this we would need to replace the keyname in the certBlob with one of the names listed. - for _, certificate := range *r.sshCertificates { + for _, certificate := range r.sshCertificates { keys = append(keys, &agent.Key{ Format: certificate.Certificate.Type(), Blob: certificate.Certificate.Marshal(), Comment: "cert " + certificate.Comment}) } - for _, algorithmSigner := range *r.sshAlgorithmSigners { + for _, algorithmSigner := range r.sshAlgorithmSigners { keys = append(keys, &agent.Key{ Format: algorithmSigner.Signer.PublicKey().Type(), Blob: algorithmSigner.Signer.PublicKey().Marshal(), @@ -94,7 +94,7 @@ func (r *sshAlgorithmSignerKeyring) Sign(key ssh.PublicKey, data []byte) (*ssh.S func (r *sshAlgorithmSignerKeyring) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { wanted := key.Marshal() - for _, sshAlgorithmSigner := range *r.sshAlgorithmSigners { + for _, sshAlgorithmSigner := range r.sshAlgorithmSigners { pubKeyBlob := sshAlgorithmSigner.Signer.PublicKey().Marshal() if bytes.Equal(pubKeyBlob, wanted) { if flags == 0 { From 9716ed38d193792cf04c2dd945d9c7d85141d0a1 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 18 May 2020 21:47:49 +0200 Subject: [PATCH 21/32] Fix more TODOs --- cmd/authwrapper/main.go | 1 - cmd/authwrapper/setup.go | 21 ++++++++++++++++----- kms/google/kms-signer.go | 2 -- server/main.go | 21 ++++++++++++++++++--- server/utils.go | 19 ++++++++++++++++--- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/cmd/authwrapper/main.go b/cmd/authwrapper/main.go index 07cd8be..eb4d500 100644 --- a/cmd/authwrapper/main.go +++ b/cmd/authwrapper/main.go @@ -15,7 +15,6 @@ func main() { log.Fatalf(": %v", err) } - // TODO: Default to port if nothing has been set if config.SSHCaKeyPath != "" && config.SSHSigningServerAddress != "" { caPublickey, err := startSigningServer( config.SSHCaKeyPath, diff --git a/cmd/authwrapper/setup.go b/cmd/authwrapper/setup.go index 8683bbb..211357c 100644 --- a/cmd/authwrapper/setup.go +++ b/cmd/authwrapper/setup.go @@ -39,7 +39,7 @@ func parseEnvironment() (*Config, error) { config := &Config{ Command: os.Getenv("WRAP_COMMAND"), Args: os.Args[1:], - RequestedPrincipals: strings.Split(os.Getenv("PRINCIPALS"), ","), + RequestedPrincipals: strings.Split(os.Getenv("SSH_PRINCIPALS"), ","), SSHKeyPath: os.Getenv("SSH_KEY_PATH"), SSHKeyPassword: os.Getenv("SSH_KEY_PASSWORD"), SSHSigningServerURL: os.Getenv("SSH_SIGNING_SERVER_URL"), @@ -88,7 +88,12 @@ func parseEnvironment() (*Config, error) { } } - // TODO: Do more config error validation + if config.SSHSigningServerAddress != "" || config.SSHCaAuthorizedKeysPath != "" || config.SSHCaKeyPath != "" { + if config.SSHSigningServerAddress == "" || config.SSHCaAuthorizedKeysPath == "" || config.SSHCaKeyPath == "" { + return nil, fmt.Errorf("SSH_CA_KEY_PATH, SSH_CA_AUTHORIZED_KEYS_PATH, SSH_SIGNING_SERVER_LISTEN_ADDRESS needs to be provided") + } + + } if config.SSHSigningServerURL != "" && len(config.RequestedPrincipals) == 0 { return nil, fmt.Errorf("When SSH_SIGNING_SERVER_URL is set a list of principals needs to be provided") @@ -162,19 +167,25 @@ func setupKeyring(config *Config) (agent.ExtendedAgent, error) { } if config.SSHSigningServerURL != "" { - // TODO: Do something with the errors var errors []error for _, signer := range signers { userCert, err := fetchUserCert(config.SSHSigningServerURL, signer.Signer, config.Command, config.Args, config.RequestedPrincipals) if err != nil { - errors = append(errors, err) + errors = append(errors, fmt.Errorf("fetchUserCert for %s failed: %v", signer.Comment, err)) continue } certificates = append(certificates, sshagent.SSHCertificate{ Certificate: userCert, - Comment: "key " + config.SSHKeyPath, + Comment: "key " + signer.Comment, }) } + if len(certificates) == 0 { + errStr := "" + for _, err := range errors { + errStr += err.Error() + } + return nil, fmt.Errorf("Failed to fetch a user cert:\n" + errStr) + } } return sshagent.NewSSHAlgorithmSignerKeyring(signers, certificates) diff --git a/kms/google/kms-signer.go b/kms/google/kms-signer.go index 1da7f40..3e86a8b 100644 --- a/kms/google/kms-signer.go +++ b/kms/google/kms-signer.go @@ -1,7 +1,5 @@ package google -// TODO: Move to google kms package instead - import ( "context" "crypto" diff --git a/server/main.go b/server/main.go index 1d7874a..1fccc86 100644 --- a/server/main.go +++ b/server/main.go @@ -85,13 +85,28 @@ func (s *SigningServer) VerifyCertificateRequest(certRequest *CertificateRequest if err != nil { return nil, err } - // TODO: Check if challenge expired + now := time.Now().UTC() + + // Check if challenge expired + issueTimeStamp, err := time.Parse(time.RFC3339Nano, value.Timestamp) + if err != nil { + return nil, err + } + if issueTimeStamp.After(now.Add(30 * time.Second)) { + return nil, fmt.Errorf("challenge expired") + } + + // Fetch key from allowed map allowedKey := s.allowedKeysMap[certRequest.PublicKey] if allowedKey == nil { return nil, nil } - // TODO: Check if allowedKey is expired? + + // Disallowed if the key expired + if allowedKey.ExpiresAt.Before(now) { + return nil, fmt.Errorf("key expired") + } payload := GenerateSigningPayload(certRequest) @@ -149,7 +164,7 @@ func (s *SigningServer) GenerateChallenge() (challenge *Challenge, err error) { } jsonBytes, err := json.Marshal(&challengeValue{ - Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05.999Z"), + Timestamp: time.Now().UTC().Format(time.RFC3339Nano), Random: randomBytes, }) diff --git a/server/utils.go b/server/utils.go index 9890ee6..390d3f3 100644 --- a/server/utils.go +++ b/server/utils.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "fmt" "strings" + "time" "golang.org/x/crypto/ssh" ) @@ -22,6 +23,7 @@ func GenerateRamdomBytes(length int) (value []byte, err error) { type AllowedKey struct { Index int Key ssh.PublicKey + ExpiresAt time.Time Comment string Principals []string ValidBefore uint64 @@ -36,6 +38,10 @@ func ParseAuthorizedKeys(lines []string) ([]AllowedKey, error) { // http://man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys for i, line := range lines { + if strings.HasPrefix(line, "#") { + continue + } + publicKey, comment, options, _, err := ssh.ParseAuthorizedKey([]byte(line)) if err != nil { return nil, fmt.Errorf("failed to parse line '%s': %v", line, err) @@ -44,7 +50,8 @@ func ParseAuthorizedKeys(lines []string) ([]AllowedKey, error) { key := AllowedKey{ Index: i, Key: publicKey, - ValidBefore: ssh.CertTimeInfinity, // TODO: Set a better value // uint64(time.Now().Add(time.Minute * 60).Unix()), + ExpiresAt: time.Unix(1<<63-62135596801, 999999999), // MaxTime + ValidBefore: uint64(time.Now().Add(time.Minute * 60).Unix()), Comment: comment, Principals: []string{}, Options: map[string]string{}, @@ -64,10 +71,16 @@ func ParseAuthorizedKeys(lines []string) ([]AllowedKey, error) { case "agent-forwarding": key.Extensions["permit-agent-forwarding"] = value case "command": - // TODO: Don't allow empty commands + if value == "" { + return nil, fmt.Errorf("empty command not allowed") + } key.Options["force-command"] = value case "expiry-time": - // TODO: Use this to ignore key after date + expiresAt, err := time.Parse("2006010215040599", value) + if err != nil { + return nil, fmt.Errorf("expiry-time not valid format %s", value) + } + key.ExpiresAt = expiresAt case "from": // TODO: Convert wildcard matching to CIDR address/masklen notation key.Options["source-address"] = value From 38b2bead8165bae4feeb4c473fb5b5c9ad948f41 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 18 May 2020 22:46:15 +0200 Subject: [PATCH 22/32] Fix command handling --- cmd/authwrapper/main.go | 8 ++++++++ cmd/authwrapper/setup.go | 12 ++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cmd/authwrapper/main.go b/cmd/authwrapper/main.go index eb4d500..cffd82b 100644 --- a/cmd/authwrapper/main.go +++ b/cmd/authwrapper/main.go @@ -28,6 +28,14 @@ func main() { pubkeyString := strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(caPublickey)), "\n") fmt.Fprintf(os.Stderr, "Starting signing server on %s with key:", config.SSHSigningServerAddress) fmt.Fprintf(os.Stderr, "%s %s\n", pubkeyString, "ca "+config.SSHCaKeyPath) + if config.Command == "" { + // Wait until we get killed + select {} + } + } + + if config.Command == "" { + log.Fatalf("auth-wrapper cmd args") } agent, err := setupKeyring(config) diff --git a/cmd/authwrapper/setup.go b/cmd/authwrapper/setup.go index 211357c..7cdbc50 100644 --- a/cmd/authwrapper/setup.go +++ b/cmd/authwrapper/setup.go @@ -35,10 +35,11 @@ var principalsFlag = flag.String("principals", "", "requested principals") func parseEnvironment() (*Config, error) { flag.Parse() + args := flag.Args() config := &Config{ Command: os.Getenv("WRAP_COMMAND"), - Args: os.Args[1:], + Args: args, RequestedPrincipals: strings.Split(os.Getenv("SSH_PRINCIPALS"), ","), SSHKeyPath: os.Getenv("SSH_KEY_PATH"), SSHKeyPassword: os.Getenv("SSH_KEY_PASSWORD"), @@ -79,12 +80,11 @@ func parseEnvironment() (*Config, error) { os.Setenv("PATH", cleanedPath) config.Command = processName - } else { - if len(os.Args) < 2 { - return nil, fmt.Errorf("auth-wrapper cmd args") + } else if len(config.Args) > 0 { + config.Command = args[0] + if len(config.Args) > 1 { + config.Args = args[1:] } - config.Command = os.Args[1] - config.Args = os.Args[2:] } } From 81a20c80f4ef2cd18130688448999a9123820ec7 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 18 May 2020 22:46:46 +0200 Subject: [PATCH 23/32] Update README --- README.md | 107 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 5f61bfa..313ff59 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # Auth wrapper -Simple wrapper that exposes an ssh-agent to all sub processes using keys from Google Cloud KMS or OpenSSH pem formated key. +Command wrapper that exposes an ssh-agent to all sub processes with keys and ssh certs backed by Google Cloud KMS or local OpenSSH pem formatted keys. -This can fx be used in CI/CD pipelines when checking code out, running package installers pulling code from private repos. +This can be used in: + +* CI/CD pipelines when checking code out, running package installers pulling code from private repos. +* Auditing and restricting access to distributed SSH servers in a central location ## How to use +### Git checkout + Git clone with key store in Google Cloud KMS: ``` bash @@ -13,75 +18,83 @@ export SSH_KEY_PATH=kms://projects/yourprojectname/locations/global/keyRings/you auth-wrapper git clone git@github.com:connectedcars/private-module.git ``` -Docker buildkit build with a key stored in Google Cloud KMS: +Git clone with local key: ``` bash -export SSH_KEY_PATH=kms://projects/yourprojectname/locations/global/keyRings/yourkeyring/cryptoKeys/ssh-key/cryptoKeyVersions/1 -export PROGRESS_NO_TRUNC=1 -export DOCKER_BUILDKIT=1 -# The strings $SSH_AUTH_SOCK and $$SSH_AUTH_SOCK will be replaced with socket in the arguments -auth-wrapper docker --progress=plain --ssh=default='\$SSH_AUTH_SOCK' . # Note the escape to make sure we don't use the shells SSH_AUTH_SOCK +export SSH_KEY_PATH=build.pem +export SSH_KEY_PASSWORD=thepassword +auth-wrapper git clone git@github.com:connectedcars/private-module.git +``` + +### SSH Certs + +Signing server: + +authorized_keys: + +``` text +restrict,command="echo hello",from="192.168.1.0/24",principals="user1,serverType:*" ecdsa-sha2-nistp256 AAAA...(copy from output of client) user1@company.com +restrict,principals="user2" ssh-rsa AAAA... user1@company.com ``` -[Dockerfile](./testdata/Dockerfile) - -Google Cloud build with Docker buildkit build: - -``` yaml -steps: - # Pull a modern version of docker - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'gcr.io/cloud-builders/docker:latest'] - # Workaround for https://github.com/moby/moby/issues/39120 - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'docker/dockerfile:experimental'] - - name: 'gcr.io/cloud-builders/docker' - args: ['pull', 'docker/dockerfile:1.0-experimental'] - # Build container injecting SSH agent socket - - name: 'gcr.io/$PROJECT_ID/auth-wrapper-docker.master:latest' - args: ['build', '--progress=plain', '--ssh=default=$$SSH_AUTH_SOCK', '-tag=gcr.io/$PROJECT_ID/$REPO_NAME.$BRANCH_NAME:$COMMIT_SHA', '.'] - env: - - "SSH_KEY_PATH=kms://projects/$PROJECT_ID/locations/global/keyRings/cloudbuilder/cryptoKeys/ssh-key/cryptoKeyVersions/1" - - "PROGRESS_NO_TRUNC=1" - - "DOCKER_BUILDKIT=1" -images: ['gcr.io/$PROJECT_ID/$REPO_NAME.$BRANCH_NAME'] +``` bash +export SSH_SIGNING_SERVER_LISTEN_ADDRESS=":3080" +export SSH_CA_KEY_PATH="kms://projects/yourprojectname/locations/global/keyRings/ssh-keys/cryptoKeys/ssh-key/cryptoKeyVersions/1" +export SSH_CA_AUTHORIZED_KEYS_PATH="authorized_keys" +auth-wrapper ``` -Git clone with local key: +Client: ``` bash -export SSH_KEY_PATH=build.pem -export SSH_KEY_PASSWORD=thepassword -auth-wrapper git clone git@github.com:connectedcars/private-module.git +export SSH_KEY_PATH=kms://projects/yourprojectname/locations/global/keyRings/yourkeyring/cryptoKeys/ssh-key/cryptoKeyVersions/1 +export SSH_SIGNING_SERVER_URL="http://localhost:3080" +auth-wrapper -p user1 ssh 1.2.3.4 +auth-wrapper -p serverType:gw ssh 1.2.3.4 # Use wildcard match +``` + +SSH Server: + +~/.ssh/authorized_keys: + +``` text +cert-authority,principals="user1,serverType:gw" ssh-rsa AAAA...(copy from output of signing server) ca key ``` ## Options -Environment variables: +### Arguments + +* -principals : Principals to request + +### Environment variables + +Client options: * SSH_KEY_PATH: Path to SSH key, can be OpenSSH PEM formated key or a url to KMS key * SSH_KEY_PASSWORD: Password to key, only used by PEM formated key * WRAP_COMMAND: Command to run with the arguments to auth-wrapper +* SSH_SIGNING_SERVER_URL: Url for the signing server +* SSH_PRINCIPALS: Principals to request + +Signing server options: + +* SSH_SIGNING_SERVER_LISTEN_ADDRESS: Listen address in the following format ":8080" +* SSH_CA_KEY_PATH: Path to CA signing key, only KMS keys supported at the moment and limited to "Elliptic Curve P-256 key +SHA256 Digest" +* SSH_CA_AUTHORIZED_KEYS_PATH": Path to authorized_keys following [AUTHORIZED_KEYS_FILE_FORMAT](http://man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT) ## Google Cloud KMS key setup Create keyring and key: ``` bash -# Create keyring for cloud build keys -gcloud kms keyrings create --location global cloudbuild +# Create keyring +gcloud kms keyrings create --location global ssh-keys # It needs to be be SHA512 as the ssh client seems to default to this hashing algorithm and KMS pairs key size and hashing algorithms for some reason. -gcloud kms keys create ssh-key --keyring cloudbuilder --location global --default-algorithm rsa-sign-pkcs1-4096-sha512 --purpose asymmetric-signing +gcloud kms keys create ssh-key --keyring ssh-keys --location global --default-algorithm rsa-sign-pkcs1-4096-sha512 --purpose asymmetric-signing # Give cloud build access to use the key -gcloud kms keys add-iam-policy-binding ssh-key --keyring=cloudbuilder --location=global --member serviceAccount:projectserviceaccount@cloudbuild.gserviceaccount.com --role roles/cloudkms.signerVerifier -``` - -Extract public key and convert to ssh format: - -``` bash -gcloud kms keys versions get-public-key 1 --key ssh-key --keyring=cloudbuilder --location=global > ssh-key.pem -# Copy the output to a github user -ssh-keygen -f ssh-key.pem -i -mPKCS8 +gcloud kms keys add-iam-policy-binding ssh-key --keyring=ssh-keys --location=global --member user@company.com --role roles/cloudkms.signerVerifier ``` ## Local key From 0595a9bb62da3c4c0c9cb008af750d316b8353dd Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 13:40:17 +0200 Subject: [PATCH 24/32] Limit max request and response size --- cmd/authwrapper/setup.go | 4 ++-- cmd/authwrapper/utils.go | 7 +++++-- server/http.go | 7 ++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/authwrapper/setup.go b/cmd/authwrapper/setup.go index 7cdbc50..3dd08f0 100644 --- a/cmd/authwrapper/setup.go +++ b/cmd/authwrapper/setup.go @@ -194,7 +194,7 @@ func setupKeyring(config *Config) (agent.ExtendedAgent, error) { func fetchUserCert(signingServerURL string, signer ssh.AlgorithmSigner, command string, args []string, principals []string) (*ssh.Certificate, error) { // GET /certificate/challenge # { value: "{ \"timestamp\": \"2020-01-01T10:00:00.000Z\" \"random\": \"...\"}", signature: "signed by CA key" } var challenge server.Challenge - err := httpJSONRequest("GET", signingServerURL+"/certificate/challenge", nil, &challenge) + err := httpJSONRequest("GET", signingServerURL+"/certificate/challenge", nil, &challenge, 1*1024*1024) if err != nil { return nil, err } @@ -213,7 +213,7 @@ func fetchUserCert(signingServerURL string, signer ssh.AlgorithmSigner, command // get back { certificate: "base64 encoded cert" } var certResponse server.CertificateResponse - err = httpJSONRequest("POST", signingServerURL+"/certificate", certRequest, &certResponse) + err = httpJSONRequest("POST", signingServerURL+"/certificate", certRequest, &certResponse, 1*1024*1024) if err != nil { return nil, err } diff --git a/cmd/authwrapper/utils.go b/cmd/authwrapper/utils.go index 4154c3c..30c525c 100644 --- a/cmd/authwrapper/utils.go +++ b/cmd/authwrapper/utils.go @@ -101,7 +101,7 @@ func runCommand(command string, args []string) (exitCode int, err error) { return 0, nil } -func httpJSONRequest(method string, url string, requestData interface{}, responseData interface{}) error { +func httpJSONRequest(method string, url string, requestData interface{}, responseData interface{}, maxResponseSize int64) error { // Convert request to JSON and wrap in io.Reader var requestBody io.Reader if requestData != nil { @@ -122,7 +122,10 @@ func httpJSONRequest(method string, url string, requestData interface{}, respons return err } defer httpResponse.Body.Close() - responseBody, err := ioutil.ReadAll(httpResponse.Body) + + // Limit size of response body we read into memory + limitedReader := &io.LimitedReader{R: httpResponse.Body, N: maxResponseSize} + responseBody, err := ioutil.ReadAll(limitedReader) if err != nil { return err } diff --git a/server/http.go b/server/http.go index 42c3e1f..f11c31c 100644 --- a/server/http.go +++ b/server/http.go @@ -3,6 +3,7 @@ package server import ( "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "path/filepath" @@ -72,7 +73,11 @@ func (s *HTTPSigningServer) getCertificateChallenge(w http.ResponseWriter, r *ht func (s *HTTPSigningServer) postCertificate(w http.ResponseWriter, r *http.Request) (jsonResponse interface{}, statusError *StatusError) { defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) + + // Limit how much of the body we read in a request + limitedReader := &io.LimitedReader{R: r.Body, N: 1 * 1024 * 1024} + + body, err := ioutil.ReadAll(limitedReader) if err != nil { return nil, &StatusError{500, err} } From 964ecf8ad331cbafeec47f9812c38a77bb2daaba Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 13:44:54 +0200 Subject: [PATCH 25/32] Read default issue cert lifetime from enviroment --- cmd/authwrapper/main.go | 10 +++++++++- cmd/authwrapper/setup.go | 2 ++ cmd/authwrapper/utils.go | 4 ++-- server/main.go | 2 +- server/utils.go | 34 +++++++++++++++++----------------- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/cmd/authwrapper/main.go b/cmd/authwrapper/main.go index cffd82b..3e23bc0 100644 --- a/cmd/authwrapper/main.go +++ b/cmd/authwrapper/main.go @@ -5,6 +5,7 @@ import ( "log" "os" "strings" + "time" "golang.org/x/crypto/ssh" ) @@ -15,12 +16,19 @@ func main() { log.Fatalf(": %v", err) } - if config.SSHCaKeyPath != "" && config.SSHSigningServerAddress != "" { + if config.SSHCaKeyPath != "" && config.SSHCaAuthorizedKeysPath != "" && config.SSHSigningServerAddress != "" { + var lifetime time.Duration = time.Hour * 1 + if config.SSHSigningLifetime != "" { + lifetime, err = time.ParseDuration(config.SSHSigningLifetime) + log.Fatalf(": %v", err) + } + caPublickey, err := startSigningServer( config.SSHCaKeyPath, config.SSHCaKeyPassword, config.SSHCaAuthorizedKeysPath, config.SSHSigningServerAddress, + lifetime, ) if err != nil { log.Fatalf("createSigningServer: %v", err) diff --git a/cmd/authwrapper/setup.go b/cmd/authwrapper/setup.go index 3dd08f0..0116af0 100644 --- a/cmd/authwrapper/setup.go +++ b/cmd/authwrapper/setup.go @@ -24,6 +24,7 @@ type Config struct { SSHKeyPath string SSHKeyPassword string SSHSigningServerURL string + SSHSigningLifetime string SSHCaKeyPath string SSHCaKeyPassword string SSHCaAuthorizedKeysPath string @@ -44,6 +45,7 @@ func parseEnvironment() (*Config, error) { SSHKeyPath: os.Getenv("SSH_KEY_PATH"), SSHKeyPassword: os.Getenv("SSH_KEY_PASSWORD"), SSHSigningServerURL: os.Getenv("SSH_SIGNING_SERVER_URL"), + SSHSigningLifetime: os.Getenv("SSH_SIGNING_LIFETIME"), SSHCaKeyPath: os.Getenv("SSH_CA_KEY_PATH"), SSHCaKeyPassword: os.Getenv("SSH_CA_KEY_PASSWORD"), SSHCaAuthorizedKeysPath: os.Getenv("SSH_CA_AUTHORIZED_KEYS_PATH"), diff --git a/cmd/authwrapper/utils.go b/cmd/authwrapper/utils.go index 30c525c..5965bd6 100644 --- a/cmd/authwrapper/utils.go +++ b/cmd/authwrapper/utils.go @@ -40,7 +40,7 @@ func runCommandWithSSHAgent(agent agent.ExtendedAgent, command string, args []st return runCommand(command, args) } -func startSigningServer(caPrivateKeyPath string, keyPassword string, authorizedKeysPath, address string) (ssh.PublicKey, error) { +func startSigningServer(caPrivateKeyPath string, keyPassword string, authorizedKeysPath, address string, defaultLifetime time.Duration) (ssh.PublicKey, error) { var caPublicKey ssh.PublicKey if strings.HasPrefix(caPrivateKeyPath, "kms://") { var err error @@ -61,7 +61,7 @@ func startSigningServer(caPrivateKeyPath string, keyPassword string, authorizedK return nil, fmt.Errorf("failed readLines: %v", err) } - allowedKeys, err := server.ParseAuthorizedKeys(authorizedKeysLines) + allowedKeys, err := server.ParseAuthorizedKeys(authorizedKeysLines, defaultLifetime) if err != nil { return nil, fmt.Errorf("failed parse ParseAuthorizedKeys: %v", err) } diff --git a/server/main.go b/server/main.go index 1fccc86..1f2b4fa 100644 --- a/server/main.go +++ b/server/main.go @@ -127,7 +127,7 @@ func (s *SigningServer) IssueUserCertificate(allowedKey *AllowedKey, principals CertType: ssh.UserCert, ValidPrincipals: principals, ValidAfter: 0, - ValidBefore: allowedKey.ValidBefore, + ValidBefore: uint64(time.Now().Add(allowedKey.Lifetime).Unix()), Permissions: ssh.Permissions{ CriticalOptions: allowedKey.Options, Extensions: allowedKey.Extensions, diff --git a/server/utils.go b/server/utils.go index 390d3f3..21e5d4a 100644 --- a/server/utils.go +++ b/server/utils.go @@ -21,18 +21,18 @@ func GenerateRamdomBytes(length int) (value []byte, err error) { // AllowedKey contains the allowed values for this key type AllowedKey struct { - Index int - Key ssh.PublicKey - ExpiresAt time.Time - Comment string - Principals []string - ValidBefore uint64 - Options map[string]string - Extensions map[string]string + Index int + Key ssh.PublicKey + ExpiresAt time.Time + Lifetime time.Duration + Comment string + Principals []string + Options map[string]string + Extensions map[string]string } // ParseAuthorizedKeys to []AllowedCertKey format -func ParseAuthorizedKeys(lines []string) ([]AllowedKey, error) { +func ParseAuthorizedKeys(lines []string, defaultLifetime time.Duration) ([]AllowedKey, error) { keys := []AllowedKey{} // http://man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT @@ -48,14 +48,14 @@ func ParseAuthorizedKeys(lines []string) ([]AllowedKey, error) { } key := AllowedKey{ - Index: i, - Key: publicKey, - ExpiresAt: time.Unix(1<<63-62135596801, 999999999), // MaxTime - ValidBefore: uint64(time.Now().Add(time.Minute * 60).Unix()), - Comment: comment, - Principals: []string{}, - Options: map[string]string{}, - Extensions: map[string]string{}, + Index: i, + Key: publicKey, + ExpiresAt: time.Unix(1<<63-62135596801, 999999999), // MaxTime + Lifetime: defaultLifetime, // TODO: Add this as option or encode it in the comment field + Comment: comment, + Principals: []string{}, + Options: map[string]string{}, + Extensions: map[string]string{}, } restricted := false From 3a3af235ac65343584334fdefa77dad4adb17e86 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 13:45:40 +0200 Subject: [PATCH 26/32] Update documentation --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 313ff59..d2c1279 100644 --- a/README.md +++ b/README.md @@ -30,21 +30,34 @@ auth-wrapper git clone git@github.com:connectedcars/private-module.git Signing server: +The signing server issues a certificate based on an allow list in authorized keys file format: + +http://man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT + +Example file: + authorized_keys: ``` text -restrict,command="echo hello",from="192.168.1.0/24",principals="user1,serverType:*" ecdsa-sha2-nistp256 AAAA...(copy from output of client) user1@company.com -restrict,principals="user2" ssh-rsa AAAA... user1@company.com +# Only allow this public key access from 192.168.1.0/24 and to run command "echo hello" with principal name "user1,serverType" +restrict,command="echo hello",from="192.168.1.0/24",principals="user1,serverType" ecdsa-sha2-nistp256 AAAA...C (copy from output of client) user1@company.com +# Only allow this public key access with principal name "user2" +restrict,principals="user2" ssh-rsa AAAA...D(copy from output of client) user2@company.com +# Only allow sftp access with principal name "user3" +restrict,principals="user3",command=internal-sftp AAAA...E (copy from output of client) user3@company.com ``` +Starting the server: + ``` bash export SSH_SIGNING_SERVER_LISTEN_ADDRESS=":3080" export SSH_CA_KEY_PATH="kms://projects/yourprojectname/locations/global/keyRings/ssh-keys/cryptoKeys/ssh-key/cryptoKeyVersions/1" export SSH_CA_AUTHORIZED_KEYS_PATH="authorized_keys" +export SSH_SIGNING_LIFETIME="60m" auth-wrapper ``` -Client: +Using the client: ``` bash export SSH_KEY_PATH=kms://projects/yourprojectname/locations/global/keyRings/yourkeyring/cryptoKeys/ssh-key/cryptoKeyVersions/1 @@ -55,6 +68,8 @@ auth-wrapper -p serverType:gw ssh 1.2.3.4 # Use wildcard match SSH Server: +To configure a SSH server to trust the signing server CA for a specific user: + ~/.ssh/authorized_keys: ``` text From 6c75a56075c2ad548d1c0d081c05f5708cb8d286 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 13:46:29 +0200 Subject: [PATCH 27/32] Added TODOs and documentation --- cmd/authwrapper/setup.go | 1 + server/main.go | 1 + sshagent/keyring.go | 2 ++ 3 files changed, 4 insertions(+) diff --git a/cmd/authwrapper/setup.go b/cmd/authwrapper/setup.go index 0116af0..96da471 100644 --- a/cmd/authwrapper/setup.go +++ b/cmd/authwrapper/setup.go @@ -170,6 +170,7 @@ func setupKeyring(config *Config) (agent.ExtendedAgent, error) { if config.SSHSigningServerURL != "" { var errors []error + // TODO: support fetching new certs when they expire for _, signer := range signers { userCert, err := fetchUserCert(config.SSHSigningServerURL, signer.Signer, config.Command, config.Args, config.RequestedPrincipals) if err != nil { diff --git a/server/main.go b/server/main.go index 1f2b4fa..8c732bd 100644 --- a/server/main.go +++ b/server/main.go @@ -59,6 +59,7 @@ type SigningServer struct { // NewSigningServer creates a new server func NewSigningServer(caKey ssh.Signer, allowedKeys []AllowedKey) *SigningServer { var allowedKeysMap = map[string]*AllowedKey{} + for i, allowedKey := range allowedKeys { pubkeyString := strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(allowedKey.Key)), "\n") allowedKeysMap[pubkeyString] = &allowedKeys[i] diff --git a/sshagent/keyring.go b/sshagent/keyring.go index 5c4dae3..81d9963 100644 --- a/sshagent/keyring.go +++ b/sshagent/keyring.go @@ -70,6 +70,8 @@ func (r *sshAlgorithmSignerKeyring) List() ([]*agent.Key, error) { // TODO: the go lang ssh cert implementation does not support forcing rsa-sha2-256-cert-v01@openssh.com or rsa-sha2-512-cert-v01@openssh.com // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD // To fix this we would need to replace the keyname in the certBlob with one of the names listed. + // This seems to be fixed in a newer go version, when this is merged: + // https://github.com/golang/go/issues/37278 for _, certificate := range r.sshCertificates { keys = append(keys, &agent.Key{ Format: certificate.Certificate.Type(), From 8d027202133d422a6665ad49350357950f5aba2a Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 13:47:00 +0200 Subject: [PATCH 28/32] Error on duplicate pub keys --- server/utils.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/utils.go b/server/utils.go index 21e5d4a..1dcd613 100644 --- a/server/utils.go +++ b/server/utils.go @@ -37,6 +37,8 @@ func ParseAuthorizedKeys(lines []string, defaultLifetime time.Duration) ([]Allow // http://man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys + + seenKeys := make(map[string]bool) for i, line := range lines { if strings.HasPrefix(line, "#") { continue @@ -47,6 +49,13 @@ func ParseAuthorizedKeys(lines []string, defaultLifetime time.Duration) ([]Allow return nil, fmt.Errorf("failed to parse line '%s': %v", line, err) } + // Return error if there are duplicates + strPublicKey := string(ssh.MarshalAuthorizedKey(publicKey)) + if seenKeys[strPublicKey] { + return nil, fmt.Errorf("public key is listed more than once '%s': %v", line, err) + } + seenKeys[strPublicKey] = true + key := AllowedKey{ Index: i, Key: publicKey, From 253dc0cf3a2298b81f6aa06a862e9adcde0a58c4 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 13:47:14 +0200 Subject: [PATCH 29/32] Do more input valication --- server/http.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server/http.go b/server/http.go index f11c31c..77a8011 100644 --- a/server/http.go +++ b/server/http.go @@ -88,7 +88,7 @@ func (s *HTTPSigningServer) postCertificate(w http.ResponseWriter, r *http.Reque return nil, &StatusError{400, err} } - // Validate input + // Validate CertificateRequest input if certRequest.Challenge == nil { return nil, &StatusError{400, fmt.Errorf("challenge not set")} } @@ -98,6 +98,17 @@ func (s *HTTPSigningServer) postCertificate(w http.ResponseWriter, r *http.Reque if certRequest.Signature == nil { return nil, &StatusError{400, fmt.Errorf("signature not set")} } + if certRequest.Args == nil { + return nil, &StatusError{400, fmt.Errorf("args not set")} + } + + // Validate certRequest.Challenge + if certRequest.Challenge.Signature == nil { + return nil, &StatusError{400, fmt.Errorf("challenge.signature not set")} + } + if certRequest.Challenge.Value == nil { + return nil, &StatusError{400, fmt.Errorf("challenge.value not set")} + } // Check if this request is allowed allowedKey, err := s.signingServer.VerifyCertificateRequest(&certRequest) From de05c47e1042f2e417c8b873b8da806c230cc1cf Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 13:48:23 +0200 Subject: [PATCH 30/32] Ignore authorized_keys file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 728265e..a2ab59f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ /auth-wrapper vendor /dist +authorized_keys From 5d821b1b8f2ae90e3f5b7044fc030b5c51b1608e Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 14:49:00 +0200 Subject: [PATCH 31/32] Fix err check --- cmd/authwrapper/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/authwrapper/main.go b/cmd/authwrapper/main.go index 3e23bc0..52b37a6 100644 --- a/cmd/authwrapper/main.go +++ b/cmd/authwrapper/main.go @@ -20,7 +20,9 @@ func main() { var lifetime time.Duration = time.Hour * 1 if config.SSHSigningLifetime != "" { lifetime, err = time.ParseDuration(config.SSHSigningLifetime) - log.Fatalf(": %v", err) + if err != nil { + log.Fatalf(": %v", err) + } } caPublickey, err := startSigningServer( From f7a20219e59ac9797bc11866c9fbfb5cede0abf0 Mon Sep 17 00:00:00 2001 From: Troels Liebe Bentsen Date: Mon, 27 Jul 2020 15:09:03 +0200 Subject: [PATCH 32/32] Add server image --- Dockerfile | 16 ++++++++++++++++ cloudbuild.yaml | 10 ++++++++++ cmd/authwrapper/setup.go | 1 - 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7a771eb..662bc3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,22 @@ ENV GO111MODULE=on RUN CGO_ENABLED=0 GOOS=linux go build -o auth-wrapper -ldflags "-X 'main.versionString=$VERSION'" ./cmd/authwrapper +RUN echo nobody:x:65534:65534:nobody:/: > password.minimal + +# +# Auth-wrapper server image +# +FROM scratch as main + +ARG SSH_KEY_PATH + +COPY --from=builder /app/auth-wrapper /opt/bin/auth-wrapper +COPY --from=builder /app/password.minimal /etc/password + +USER nobody + +ENTRYPOINT ["/opt/bin/auth-wrapper"] + # # Authwrapped git with KMS keys # diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 3c11947..683825b 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -2,6 +2,15 @@ steps: # # Build KMS auth wrappers # + # Build auth wrapped server container + - name: 'gcr.io/cloud-builders/docker' + args: [ + 'build', + '--target=main', + '--tag=gcr.io/$PROJECT_ID/$REPO_NAME.$BRANCH_NAME:$COMMIT_SHA', + '--tag=gcr.io/$PROJECT_ID/$REPO_NAME.$BRANCH_NAME:latest', + '.' + ] # Build auth wrapped git - name: 'gcr.io/cloud-builders/docker' args: [ @@ -49,6 +58,7 @@ secrets: secretEnv: SSH_KEY_PASSWORD: CiQAg7wCPfO2Tf9mtZoFWjAtX7whQ481af3gyGdM9WNK26B74UkSUQBefMgeHNh0KTsGybKReXDsFcbmed7f5sw97zSe9cswpKogENM5Ye0jiIu6NfebUpCnmJ9HVHmD/yBknlW4nn1VXBs7HYGiBSFZ52i2HyEopw== images: [ + 'gcr.io/$PROJECT_ID/$REPO_NAME.$BRANCH_NAME', 'gcr.io/$PROJECT_ID/$REPO_NAME-git.$BRANCH_NAME', 'gcr.io/$PROJECT_ID/$REPO_NAME-git-local.$BRANCH_NAME' ] diff --git a/cmd/authwrapper/setup.go b/cmd/authwrapper/setup.go index 96da471..2f74d75 100644 --- a/cmd/authwrapper/setup.go +++ b/cmd/authwrapper/setup.go @@ -94,7 +94,6 @@ func parseEnvironment() (*Config, error) { if config.SSHSigningServerAddress == "" || config.SSHCaAuthorizedKeysPath == "" || config.SSHCaKeyPath == "" { return nil, fmt.Errorf("SSH_CA_KEY_PATH, SSH_CA_AUTHORIZED_KEYS_PATH, SSH_SIGNING_SERVER_LISTEN_ADDRESS needs to be provided") } - } if config.SSHSigningServerURL != "" && len(config.RequestedPrincipals) == 0 {