mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 05:55:42 +08:00
216 lines
6.8 KiB
Go
216 lines
6.8 KiB
Go
/*
|
|
Copyright 2021 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package sshutils defines several functions and types used across the
|
|
// Teleport API and other Teleport packages when working with SSH.
|
|
package sshutils
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"runtime"
|
|
|
|
"github.com/gravitational/teleport/api/constants"
|
|
"github.com/gravitational/teleport/api/defaults"
|
|
|
|
"github.com/gravitational/trace"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/agent"
|
|
)
|
|
|
|
// ParseCertificate parses an SSH certificate from the authorized_keys format.
|
|
func ParseCertificate(buf []byte) (*ssh.Certificate, error) {
|
|
k, _, _, _, err := ssh.ParseAuthorizedKey(buf)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
cert, ok := k.(*ssh.Certificate)
|
|
if !ok {
|
|
return nil, trace.BadParameter("not an SSH certificate")
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
// ProxyClientSSHConfig returns an ssh.ClientConfig with SSH credentials from this
|
|
// Key and HostKeyCallback matching SSH CAs in the Key.
|
|
//
|
|
// The config is set up to authenticate to proxy with the first available principal.
|
|
//
|
|
func ProxyClientSSHConfig(sshCert, privKey []byte, caCerts [][]byte) (*ssh.ClientConfig, error) {
|
|
cert, err := ParseCertificate(sshCert)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "failed to extract username from SSH certificate")
|
|
}
|
|
|
|
authMethod, err := AsAuthMethod(cert, privKey)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "failed to convert key pair to auth method")
|
|
}
|
|
|
|
hostKeyCallback, err := HostKeyCallback(caCerts)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "failed to convert certificate authorities to HostKeyCallback")
|
|
}
|
|
|
|
// The KeyId is not always a valid principal, so we use the first valid principal instead.
|
|
user := cert.KeyId
|
|
if len(cert.ValidPrincipals) > 0 {
|
|
user = cert.ValidPrincipals[0]
|
|
}
|
|
|
|
return &ssh.ClientConfig{
|
|
User: user,
|
|
Auth: []ssh.AuthMethod{authMethod},
|
|
HostKeyCallback: hostKeyCallback,
|
|
Timeout: defaults.DefaultDialTimeout,
|
|
}, nil
|
|
}
|
|
|
|
// AsSigner returns an ssh.Signer from raw marshaled key and certificate.
|
|
func AsSigner(sshCert *ssh.Certificate, privKey []byte) (ssh.Signer, error) {
|
|
keys, err := AsAgentKeys(sshCert, privKey)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
signer, err := ssh.NewSignerFromKey(keys[0].PrivateKey)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
signer, err = ssh.NewCertSigner(keys[0].Certificate, signer)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return signer, nil
|
|
}
|
|
|
|
// AsAuthMethod returns an "auth method" interface, a common abstraction
|
|
// used by Golang SSH library. This is how you actually use a Key to feed
|
|
// it into the SSH lib.
|
|
func AsAuthMethod(sshCert *ssh.Certificate, privKey []byte) (ssh.AuthMethod, error) {
|
|
signer, err := AsSigner(sshCert, privKey)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return ssh.PublicKeys(signer), nil
|
|
}
|
|
|
|
// AsAgentKeys converts Key struct to a []*agent.AddedKey. All elements
|
|
// of the []*agent.AddedKey slice need to be loaded into the agent!
|
|
func AsAgentKeys(sshCert *ssh.Certificate, privKey []byte) ([]agent.AddedKey, error) {
|
|
// unmarshal private key bytes into a *rsa.PrivateKey
|
|
privateKey, err := ssh.ParseRawPrivateKey(privKey)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// put a teleport identifier along with the teleport user into the comment field
|
|
comment := fmt.Sprintf("teleport:%v", sshCert.KeyId)
|
|
|
|
// On Windows, return the certificate with the private key embedded.
|
|
if runtime.GOOS == constants.WindowsOS {
|
|
return []agent.AddedKey{
|
|
{
|
|
PrivateKey: privateKey,
|
|
Certificate: sshCert,
|
|
Comment: comment,
|
|
LifetimeSecs: 0,
|
|
ConfirmBeforeUse: false,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// On Unix, return the certificate (with embedded private key) as well as
|
|
// a private key.
|
|
//
|
|
// This is done because OpenSSH clients older than OpenSSH 7.3/7.3p1
|
|
// (2016-08-01) have a bug in how they use certificates that have been loaded
|
|
// in an agent. Specifically when you add a certificate to an agent, you can't
|
|
// just embed the private key within the certificate, you have to add the
|
|
// certificate and private key to the agent separately. Teleport works around
|
|
// this behavior to ensure OpenSSH interoperability.
|
|
//
|
|
// For more details see the following: https://bugzilla.mindrot.org/show_bug.cgi?id=2550
|
|
// WARNING: callers expect the returned slice to be __exactly as it is__
|
|
return []agent.AddedKey{
|
|
{
|
|
PrivateKey: privateKey,
|
|
Certificate: sshCert,
|
|
Comment: comment,
|
|
LifetimeSecs: 0,
|
|
ConfirmBeforeUse: false,
|
|
},
|
|
{
|
|
PrivateKey: privateKey,
|
|
Certificate: nil,
|
|
Comment: comment,
|
|
LifetimeSecs: 0,
|
|
ConfirmBeforeUse: false,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// HostKeyCallback returns an ssh.HostKeyCallback that validates host
|
|
// keys/certs against SSH CAs in the Key.
|
|
//
|
|
// If not CAs are present in the Key, the returned ssh.HostKeyCallback is nil.
|
|
// This causes golang.org/x/crypto/ssh to prompt the user to verify host key
|
|
// fingerprint (same as OpenSSH does for an unknown host).
|
|
func HostKeyCallback(caCerts [][]byte) (ssh.HostKeyCallback, error) {
|
|
var trustedKeys []ssh.PublicKey
|
|
for _, caCert := range caCerts {
|
|
for {
|
|
_, _, publicKey, _, bytes, err := ssh.ParseKnownHosts(caCert)
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return nil, trace.Wrap(err, "failed parsing CA cert: %v; raw CA cert line: %q", err, caCert)
|
|
}
|
|
trustedKeys = append(trustedKeys, publicKey)
|
|
caCert = bytes
|
|
}
|
|
}
|
|
|
|
// No CAs are provided, return a nil callback which will prompt the user
|
|
// for trust.
|
|
if len(trustedKeys) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return func(host string, a net.Addr, hostKey ssh.PublicKey) error {
|
|
clusterCert, ok := hostKey.(*ssh.Certificate)
|
|
if ok {
|
|
hostKey = clusterCert.SignatureKey
|
|
}
|
|
for _, trustedKey := range trustedKeys {
|
|
if KeysEqual(trustedKey, hostKey) {
|
|
return nil
|
|
}
|
|
}
|
|
return trace.AccessDenied("host %v is untrusted or Teleport CA has been rotated; try getting new credentials by logging in again ('tsh login') or re-exporting the identity file ('tctl auth sign' or 'tsh login -o'), depending on how you got them initially", host)
|
|
}, nil
|
|
}
|
|
|
|
// KeysEqual is constant time compare of the keys to avoid timing attacks
|
|
func KeysEqual(ak, bk ssh.PublicKey) bool {
|
|
a := ssh.Marshal(ak)
|
|
b := ssh.Marshal(bk)
|
|
return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
|
|
}
|