Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/client/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ func writeProfile(t *testing.T, p *profile.Profile) {
require.NoError(t, os.MkdirAll(p.KeyDir(), 0700))
require.NoError(t, os.MkdirAll(p.ProxyKeyDir(), 0700))
require.NoError(t, os.MkdirAll(p.TLSClusterCASDir(), 0700))
require.NoError(t, os.WriteFile(p.UserKeyPath(), keyPEM, 0600))
require.NoError(t, os.WriteFile(p.UserSSHKeyPath(), keyPEM, 0600))
require.NoError(t, os.WriteFile(p.UserTLSKeyPath(), keyPEM, 0600))
require.NoError(t, os.WriteFile(p.TLSCertPath(), tlsCert, 0600))
require.NoError(t, os.WriteFile(p.TLSCAPathCluster(p.SiteName), tlsCACert, 0600))
require.NoError(t, os.WriteFile(p.KnownHostsPath(), sshCACert, 0600))
Expand Down
15 changes: 10 additions & 5 deletions api/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func (p *Profile) Name() string {

// TLSConfig returns the profile's associated TLSConfig.
func (p *Profile) TLSConfig() (*tls.Config, error) {
cert, err := keys.LoadX509KeyPair(p.TLSCertPath(), p.UserKeyPath())
cert, err := keys.LoadX509KeyPair(p.TLSCertPath(), p.UserTLSKeyPath())
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -251,7 +251,7 @@ func (p *Profile) SSHClientConfig() (*ssh.ClientConfig, error) {
return nil, trace.Wrap(err)
}

priv, err := keys.LoadPrivateKey(p.UserKeyPath())
priv, err := keys.LoadPrivateKey(p.UserSSHKeyPath())
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -449,9 +449,14 @@ func (p *Profile) ProxyKeyDir() string {
return keypaths.ProxyKeyDir(p.Dir, p.Name())
}

// UserKeyPath returns the path to the profile's private key.
func (p *Profile) UserKeyPath() string {
return keypaths.UserKeyPath(p.Dir, p.Name(), p.Username)
// UserSSHKeyPath returns the path to the profile's SSH private key.
func (p *Profile) UserSSHKeyPath() string {
return keypaths.UserSSHKeyPath(p.Dir, p.Name(), p.Username)
}

// UserTLSKeyPath returns the path to the profile's TLS private key.
func (p *Profile) UserTLSKeyPath() string {
return keypaths.UserTLSKeyPath(p.Dir, p.Name(), p.Username)
}

// TLSCertPath returns the path to the profile's TLS certificate.
Expand Down
8 changes: 8 additions & 0 deletions api/types/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ type AuthPreference interface {
// SetAllowHeadless sets the value of the allow headless setting.
SetAllowHeadless(b bool)

// SetRequireMFAType sets the type of MFA requirement enforced for this cluster.
SetRequireMFAType(RequireMFAType)
// GetRequireMFAType returns the type of MFA requirement enforced for this cluster.
GetRequireMFAType() RequireMFAType

// GetPrivateKeyPolicy returns the configured private key policy for the cluster.
GetPrivateKeyPolicy() keys.PrivateKeyPolicy

Expand Down Expand Up @@ -412,6 +415,11 @@ func (c *AuthPreferenceV2) SetAllowHeadless(b bool) {
c.Spec.AllowHeadless = NewBoolOption(b)
}

// SetRequireMFAType sets the type of MFA requirement enforced for this cluster.
func (c *AuthPreferenceV2) SetRequireMFAType(t RequireMFAType) {
c.Spec.RequireMFAType = t
}

// GetRequireMFAType returns the type of MFA requirement enforced for this cluster.
func (c *AuthPreferenceV2) GetRequireMFAType() RequireMFAType {
return c.Spec.RequireMFAType
Expand Down
2 changes: 1 addition & 1 deletion api/types/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ func (ws *WebSessionV2) SetSSHPriv(priv []byte) {

// GetTLSPriv returns private TLS key.
func (ws *WebSessionV2) GetTLSPriv() []byte {
// TODO(nklaassen): DELETE IN 18.0.0
// TODO(nklaassen): DELETE IN 18.0.0 when all auth servers are writing web session TLS key.
if ws.Spec.TLSPriv == nil {
// An older auth instance may have written this web session before the
// SSH and TLS keys were split.
Expand Down
23 changes: 16 additions & 7 deletions api/utils/keypaths/keypaths.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ const (
// └── keys --> session keys directory
// ├── one.example.com --> Proxy hostname
// │ ├── certs.pem --> TLS CA certs for the Teleport CA
// │ ├── foo --> Private Key for user "foo"
// │ ├── foo.pub --> Public Key
// │ ├── foo.key --> TLS Private Key for user "foo"
// │ ├── foo.crt --> TLS client certificate for Auth Server
// │ ├── foo --> SSH Private Key for user "foo"
// │ ├── foo.pub --> SSH Public Key
// │ ├── foo.ppk --> PuTTY PPK-formatted keypair for user "foo"
// │ ├── kube_credentials.lock --> Kube credential lockfile, used to prevent excessive relogin attempts
// │ ├── foo-x509.pem --> TLS client certificate for Auth Server
// │ ├── foo-ssh --> SSH certs for user "foo"
// │ │ ├── root-cert.pub --> SSH cert for Teleport cluster "root"
// │ │ └── leaf-cert.pub --> SSH cert for Teleport cluster "leaf"
Expand Down Expand Up @@ -163,20 +164,28 @@ func ProxyKeyDir(baseDir, proxy string) string {
return filepath.Join(KeyDir(baseDir), proxy)
}

// UserKeyPath returns the path to the users's private key
// UserSSHKeyPath returns the path to the users's SSH private key
// for the given proxy.
//
// <baseDir>/keys/<proxy>/<username>.
func UserKeyPath(baseDir, proxy, username string) string {
func UserSSHKeyPath(baseDir, proxy, username string) string {
return filepath.Join(ProxyKeyDir(baseDir, proxy), username)
}

// UserTLSKeyPath returns the path to the users's TLS private key
// for the given proxy.
//
// <baseDir>/keys/<proxy>/<username>.key
func UserTLSKeyPath(baseDir, proxy, username string) string {
return filepath.Join(ProxyKeyDir(baseDir, proxy), username+fileExtTLSKey)
}

// TLSCertPath returns the path to the users's TLS certificate
// for the given proxy.
//
// <baseDir>/keys/<proxy>/<username>-x509.pem
// <baseDir>/keys/<proxy>/<username>.crt
func TLSCertPath(baseDir, proxy, username string) string {
return filepath.Join(ProxyKeyDir(baseDir, proxy), username+fileExtTLSCertLegacy)
return filepath.Join(ProxyKeyDir(baseDir, proxy), username+FileExtTLSCert)
}

// PublicKeyPath returns the path to the users's public key
Expand Down
80 changes: 73 additions & 7 deletions api/utils/keys/privatekey.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
PKCS1PrivateKeyType = "RSA PRIVATE KEY"
PKCS8PrivateKeyType = "PRIVATE KEY"
ECPrivateKeyType = "EC PRIVATE KEY"
OpenSSHPrivateKeyType = "OPENSSH PRIVATE KEY"
pivYubiKeyPrivateKeyType = "PIV YUBIKEY PRIVATE KEY"
)

Expand All @@ -57,7 +58,8 @@ type PrivateKey struct {
keyPEM []byte
}

// NewPrivateKey returns a new PrivateKey for the given crypto.Signer.
// NewPrivateKey returns a new PrivateKey for the given crypto.Signer with a
// pre-marshaled private key PEM, which may be a special PIV key PEM.
func NewPrivateKey(signer crypto.Signer, keyPEM []byte) (*PrivateKey, error) {
sshPub, err := ssh.NewPublicKey(signer.Public())
if err != nil {
Expand All @@ -71,16 +73,64 @@ func NewPrivateKey(signer crypto.Signer, keyPEM []byte) (*PrivateKey, error) {
}, nil
}

// SSHPublicKey returns the ssh.PublicKey representiation of the public key.
// NewSoftwarePrivateKey returns a new PrivateKey for a crypto.Signer.
// [signer] must be an *rsa.PrivateKey, *ecdsa.PrivateKey, or ed25519.PrivateKey.
func NewSoftwarePrivateKey(signer crypto.Signer) (*PrivateKey, error) {
sshPub, err := ssh.NewPublicKey(signer.Public())
if err != nil {
return nil, trace.Wrap(err)
}
keyPEM, err := MarshalPrivateKey(signer)
if err != nil {
return nil, trace.Wrap(err)
}
return &PrivateKey{
Signer: signer,
sshPub: sshPub,
keyPEM: keyPEM,
}, nil
}

// SSHPublicKey returns the ssh.PublicKey representation of the public key.
func (k *PrivateKey) SSHPublicKey() ssh.PublicKey {
return k.sshPub
}

// SSHPublicKey returns the ssh.PublicKey representiation of the public key.
// MarshalSSHPrivateKey returns the private key marshaled to:
// - PEM-encoded OpenSSH format for Ed25519 or ECDSA keys
// - PEM-encoded PKCS#1 for RSA keys
// - a custom PEM-encoded format for PIV keys
func (k *PrivateKey) MarshalSSHPrivateKey() ([]byte, error) {
switch k.Signer.(type) {
case ed25519.PrivateKey, *ecdsa.PrivateKey:
// OpenSSH largely does not support PKCS8 private keys, write these in
// OpenSSH format.
const comment = ""
pemBlock, err := ssh.MarshalPrivateKey(k.Signer, comment)
if err != nil {
return nil, trace.Wrap(err)
}
return pem.EncodeToMemory(pemBlock), nil
}
// Otherwise we are dealing with either a hardware key which has a custom
// format, or an RSA key which would already be in PKCS#1, which OpenSSH can
// handle.
return k.keyPEM, nil
}

// MarshalSSHPublicKey returns the public key marshaled to SSH authorized_keys format.
func (k *PrivateKey) MarshalSSHPublicKey() []byte {
return ssh.MarshalAuthorizedKey(k.sshPub)
}

// MarshalTLSPublicKey returns a PEM encoding of the public key. Encodes RSA keys
// in PKCS1 format for backward compatibility. All other key types are encoded
// in PKIX, ASN.1 DER form. Only supports *rsa.PublicKey, *ecdsa.PublicKey, and
// ed25519.PublicKey.
func (k *PrivateKey) MarshalTLSPublicKey() ([]byte, error) {
return MarshalPublicKey(k.Signer.Public())
}

// PrivateKeyPEM returns PEM encoded private key data. This may be data necessary
// to retrieve the key, such as a YubiKey serial number and slot, or it can be a
// PKCS marshaled private key.
Expand Down Expand Up @@ -192,6 +242,22 @@ func ParsePrivateKey(keyPEM []byte) (*PrivateKey, error) {
return nil, trace.BadParameter("x509.ParsePKCS8PrivateKey returned an invalid private key of type %T", priv)
}
return NewPrivateKey(cryptoSigner, keyPEM)
case OpenSSHPrivateKeyType:
priv, err := ssh.ParseRawPrivateKey(keyPEM)
if err != nil {
return nil, trace.Wrap(err)
}
cryptoSigner, ok := priv.(crypto.Signer)
if !ok {
return nil, trace.BadParameter("ssh.ParseRawPrivateKey returned an invalid private key of type %T", priv)
}
// For some reason ssh.ParseRawPrivateKey returns a *ed25519.PrivateKey
// instead of the plain ed25519.PrivateKey which is used everywhere
// else. This breaks comparisons and type switches, so explicitly convert it.
if pEdwards, ok := cryptoSigner.(*ed25519.PrivateKey); ok {
cryptoSigner = *pEdwards
}
return NewPrivateKey(cryptoSigner, keyPEM)
case pivYubiKeyPrivateKeyType:
priv, err := parseYubiKeyPrivateKeyData(block.Bytes)
if err != nil {
Expand Down Expand Up @@ -235,27 +301,27 @@ func LoadKeyPair(privFile, sshPubFile string) (*PrivateKey, error) {
return nil, trace.ConvertSystemError(err)
}

marshalledSSHPub, err := os.ReadFile(sshPubFile)
marshaledSSHPub, err := os.ReadFile(sshPubFile)
if err != nil {
return nil, trace.ConvertSystemError(err)
}

priv, err := ParseKeyPair(privPEM, marshalledSSHPub)
priv, err := ParseKeyPair(privPEM, marshaledSSHPub)
if err != nil {
return nil, trace.Wrap(err)
}
return priv, nil
}

// ParseKeyPair returns the PrivateKey for the given private and public key PEM blocks.
func ParseKeyPair(privPEM, marshalledSSHPub []byte) (*PrivateKey, error) {
func ParseKeyPair(privPEM, marshaledSSHPub []byte) (*PrivateKey, error) {
priv, err := ParsePrivateKey(privPEM)
if err != nil {
return nil, trace.Wrap(err)
}

// Verify that the private key's public key matches the expected public key.
if !bytes.Equal(ssh.MarshalAuthorizedKey(priv.SSHPublicKey()), marshalledSSHPub) {
if !bytes.Equal(ssh.MarshalAuthorizedKey(priv.SSHPublicKey()), marshaledSSHPub) {
return nil, trace.CompareFailed("the given private and public keys do not form a valid keypair")
}

Expand Down
6 changes: 4 additions & 2 deletions api/utils/keys/publickey.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ const (
PKIXPublicKeyType = "PUBLIC KEY"
)

// MarshalPublicKey returns a PEM encoding of the given public key. Encodes RSA keys in PKCS1 format for
// backward compatibility. Only supports *rsa.PublicKey, *ecdsa.PublicKey, and ed25519.PublicKey.
// MarshalPublicKey returns a PEM encoding of the given public key. Encodes RSA
// keys in PKCS1 format for backward compatibility. All other key types are
// encoded in PKIX, ASN.1 DER form. Only supports *rsa.PublicKey,
// *ecdsa.PublicKey, and ed25519.PublicKey.
func MarshalPublicKey(pub crypto.PublicKey) ([]byte, error) {
switch pubKey := pub.(type) {
case *rsa.PublicKey:
Expand Down
31 changes: 27 additions & 4 deletions integration/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package helpers

import (
"context"
"crypto"
"fmt"
"net"
"os"
Expand All @@ -44,6 +45,7 @@ import (
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/teleport/lib"
"github.com/gravitational/teleport/lib/auth"
Expand All @@ -52,6 +54,7 @@ import (
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/identityfile"
"github.com/gravitational/teleport/lib/cloud/imds"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/multiplexer"
Expand Down Expand Up @@ -189,12 +192,26 @@ func CloseAgent(teleAgent *teleagent.AgentServer, socketDirPath string) error {
}

func MustCreateUserKeyRing(t *testing.T, tc *TeleInstance, username string, ttl time.Duration) *client.KeyRing {
keyRing, err := client.GenerateRSAKeyRing()
sshKey, tlsKey, err := cryptosuites.GenerateUserSSHAndTLSKey(context.Background(), func(_ context.Context) (types.SignatureAlgorithmSuite, error) {
return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil
})
require.NoError(t, err)
return mustCreateUserKeyRingWithKeys(t, tc, username, ttl, sshKey, tlsKey)
}

func mustCreateUserKeyRingWithKeys(t *testing.T, tc *TeleInstance, username string, ttl time.Duration, sshKey, tlsKey crypto.Signer) *client.KeyRing {
sshPriv, err := keys.NewSoftwarePrivateKey(sshKey)
require.NoError(t, err)
tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey)
require.NoError(t, err)
keyRing := client.NewKeyRing(sshPriv, tlsPriv)
keyRing.ClusterName = tc.Secrets.SiteName

tlsPub, err := keys.MarshalPublicKey(tlsKey.Public())
require.NoError(t, err)
sshCert, tlsCert, err := tc.Process.GetAuthServer().GenerateUserTestCerts(auth.GenerateUserTestCertsRequest{
Key: keyRing.PrivateKey.MarshalSSHPublicKey(),
SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(),
TLSPubKey: tlsPub,
Username: username,
TTL: ttl,
Compatibility: constants.CertificateFormatStandard,
Expand All @@ -212,10 +229,16 @@ func MustCreateUserKeyRing(t *testing.T, tc *TeleInstance, username string, ttl
}

func MustCreateUserIdentityFile(t *testing.T, tc *TeleInstance, username string, ttl time.Duration) string {
keyRing := MustCreateUserKeyRing(t, tc, username, ttl)
key, err := cryptosuites.GenerateKey(context.Background(), func(_ context.Context) (types.SignatureAlgorithmSuite, error) {
return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil
}, cryptosuites.UserTLS)
require.NoError(t, err)
// Identity files must use the same key for SSH and TLS.
sshKey, tlsKey := key, key
keyRing := mustCreateUserKeyRingWithKeys(t, tc, username, ttl, sshKey, tlsKey)

idPath := filepath.Join(t.TempDir(), "user_identity")
_, err := identityfile.Write(context.Background(), identityfile.WriteConfig{
_, err = identityfile.Write(context.Background(), identityfile.WriteConfig{
OutputPath: idPath,
KeyRing: keyRing,
Format: identityfile.FormatFile,
Expand Down
Loading