mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-01 16:03:55 +08:00
2d4afb6e5a
* Support Windows desktop certs in tsh * Add missing `WindowsDesktopTLSCredentials` initialization
684 lines
22 KiB
Go
684 lines
22 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/subtle"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log/slog"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/agent"
|
|
|
|
"github.com/gravitational/teleport/api/constants"
|
|
apiutils "github.com/gravitational/teleport/api/utils"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/api/utils/keys/hardwarekey"
|
|
"github.com/gravitational/teleport/api/utils/sshutils"
|
|
"github.com/gravitational/teleport/lib/auth/authclient"
|
|
"github.com/gravitational/teleport/lib/cryptosuites"
|
|
"github.com/gravitational/teleport/lib/sshca"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
// KeyRingIndex identifies a KeyRing in the store.
|
|
type KeyRingIndex struct {
|
|
// ProxyHost is the root proxy hostname that a key is associated with.
|
|
ProxyHost string
|
|
// Username is the username that a key is associated with.
|
|
Username string
|
|
// ClusterName is the cluster name that a key is associated with.
|
|
ClusterName string
|
|
}
|
|
|
|
// Check verifies the KeyRingIndex is fully specified.
|
|
func (idx KeyRingIndex) Check() error {
|
|
missingField := "keyring index field %s is not set"
|
|
if idx.ProxyHost == "" {
|
|
return trace.BadParameter(missingField, "ProxyHost")
|
|
}
|
|
if idx.Username == "" {
|
|
return trace.BadParameter(missingField, "Username")
|
|
}
|
|
if idx.ClusterName == "" {
|
|
return trace.BadParameter(missingField, "ClusterName")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Match compares this KeyRingIndex to the given matchKeyRing index.
|
|
// It will be considered a match if all non-zero elements of
|
|
// the matchKeyRing are matched by this KeyRingIndex.
|
|
func (idx KeyRingIndex) Match(matchKeyRing KeyRingIndex) bool {
|
|
return (matchKeyRing.ProxyHost == "" || matchKeyRing.ProxyHost == idx.ProxyHost) &&
|
|
(matchKeyRing.ClusterName == "" || matchKeyRing.ClusterName == idx.ClusterName) &&
|
|
(matchKeyRing.Username == "" || matchKeyRing.Username == idx.Username)
|
|
}
|
|
|
|
func (idx KeyRingIndex) LogValue() slog.Value {
|
|
return slog.GroupValue(
|
|
slog.String("proxy", idx.ProxyHost),
|
|
slog.String("cluster", idx.ClusterName),
|
|
slog.String("username", idx.Username),
|
|
)
|
|
}
|
|
|
|
func (idx KeyRingIndex) contextualKeyInfo() hardwarekey.ContextualKeyInfo {
|
|
return hardwarekey.ContextualKeyInfo{
|
|
ProxyHost: idx.ProxyHost,
|
|
Username: idx.Username,
|
|
ClusterName: idx.ClusterName,
|
|
}
|
|
}
|
|
|
|
// TLSCredential holds a signed TLS certificate and matching private key.
|
|
type TLSCredential struct {
|
|
// PrivateKey is the private key of the credential.
|
|
PrivateKey *keys.PrivateKey
|
|
// Cert is a PEM-encoded signed X509 certificate.
|
|
Cert []byte
|
|
}
|
|
|
|
// TLSCertificate returns a valid [tls.Certificate] ready to be used in a TLS
|
|
// handshake.
|
|
func (c *TLSCredential) TLSCertificate() (tls.Certificate, error) {
|
|
cert, err := c.PrivateKey.TLSCertificate(c.Cert)
|
|
return cert, trace.Wrap(err)
|
|
}
|
|
|
|
// KeyRing describes a set of client keys and certificates for a specific cluster.
|
|
type KeyRing struct {
|
|
KeyRingIndex
|
|
|
|
// SSHPrivateKey is a private key used for SSH authentication.
|
|
SSHPrivateKey *keys.PrivateKey
|
|
// Cert is an SSH client certificate.
|
|
Cert []byte
|
|
|
|
// TLSPrivateKey is a private key used for TLS authentication.
|
|
TLSPrivateKey *keys.PrivateKey
|
|
// TLSCert is a PEM encoded client TLS x509 certificate.
|
|
// It's used to authenticate to the Teleport APIs.
|
|
TLSCert []byte
|
|
|
|
// KubeTLSCredentials are TLS credentials for individual kubernetes clusters.
|
|
// Map key is a kubernetes cluster name.
|
|
KubeTLSCredentials map[string]TLSCredential
|
|
// DBTLSCredentials are TLS credentials for database access.
|
|
// Map key is the database service name.
|
|
DBTLSCredentials map[string]TLSCredential
|
|
// AppTLSCredentials are TLS credentials for application access.
|
|
// Map key is the application name.
|
|
AppTLSCredentials map[string]TLSCredential
|
|
// WindowsDesktopTLSCredentials are TLS credentials for desktop access.
|
|
// Map key is the desktop name.
|
|
WindowsDesktopTLSCredentials map[string]TLSCredential
|
|
// TrustedCerts is a list of trusted certificate authorities
|
|
TrustedCerts []authclient.TrustedCerts
|
|
}
|
|
|
|
// Copy returns a shallow copy of k, or nil if k is nil.
|
|
func (k *KeyRing) Copy() *KeyRing {
|
|
if k == nil {
|
|
return nil
|
|
}
|
|
copy := *k
|
|
return ©
|
|
}
|
|
|
|
// generateSubjectTLSKey returns a new private key with the appropriate algorithm for
|
|
// [purpose], meant to be used as the subject key for a new user cert request.
|
|
// If [k.PrivateKey] is a PIV/hardware key or an RSA key, it will be re-used.
|
|
func (k *KeyRing) generateSubjectTLSKey(ctx context.Context, tc *TeleportClient, purpose cryptosuites.KeyPurpose) (*keys.PrivateKey, error) {
|
|
if k.TLSPrivateKey.IsHardware() {
|
|
// We always re-use the root TLS key if it is a hardware key.
|
|
return k.TLSPrivateKey, nil
|
|
}
|
|
if _, isRSA := k.TLSPrivateKey.Public().(*rsa.PublicKey); isRSA {
|
|
// We always re-use the root TLS key if it is RSA (it would be expensive
|
|
// to always generate new RSA keys). If [k.PrivateKey] is RSA we must be
|
|
// using the `legacy` signature algorithm suitei and the subject keys
|
|
// should be RSA as well.
|
|
return k.TLSPrivateKey, nil
|
|
}
|
|
|
|
key, err := cryptosuites.GenerateKey(ctx, tc.GetCurrentSignatureAlgorithmSuite, purpose)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
priv, err := keys.NewPrivateKey(key)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return priv, nil
|
|
}
|
|
|
|
// NewKeyRing creates a new KeyRing for the given private keys.
|
|
func NewKeyRing(sshPriv, tlsPriv *keys.PrivateKey) *KeyRing {
|
|
return &KeyRing{
|
|
SSHPrivateKey: sshPriv,
|
|
TLSPrivateKey: tlsPriv,
|
|
KubeTLSCredentials: make(map[string]TLSCredential),
|
|
DBTLSCredentials: make(map[string]TLSCredential),
|
|
AppTLSCredentials: make(map[string]TLSCredential),
|
|
WindowsDesktopTLSCredentials: make(map[string]TLSCredential),
|
|
}
|
|
}
|
|
|
|
// RootClusterCAs returns root cluster CAs.
|
|
func (k *KeyRing) RootClusterCAs() ([][]byte, error) {
|
|
rootClusterName, err := k.RootClusterName()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
var out [][]byte
|
|
for _, cas := range k.TrustedCerts {
|
|
for _, v := range cas.TLSCertificates {
|
|
cert, err := tlsca.ParseCertificatePEM(v)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if cert.Subject.CommonName == rootClusterName {
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
}
|
|
if len(out) > 0 {
|
|
return out, nil
|
|
}
|
|
return nil, trace.NotFound("failed to find TLS CA for %q root cluster", rootClusterName)
|
|
}
|
|
|
|
// TLSCAs returns all TLS CA certificates from this KeyRing.
|
|
func (k *KeyRing) TLSCAs() (result [][]byte) {
|
|
for _, ca := range k.TrustedCerts {
|
|
result = append(result, ca.TLSCertificates...)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (k *KeyRing) KubeClientTLSConfig(cipherSuites []uint16, kubeClusterName string) (*tls.Config, error) {
|
|
rootCluster, err := k.RootClusterName()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
cred, ok := k.KubeTLSCredentials[kubeClusterName]
|
|
if !ok {
|
|
return nil, trace.NotFound("TLS certificate for kubernetes cluster %q not found", kubeClusterName)
|
|
}
|
|
|
|
tlsConfig, err := k.clientTLSConfig(cipherSuites, cred, []string{rootCluster})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
tlsConfig.ServerName = fmt.Sprintf("%s%s", constants.KubeTeleportProxyALPNPrefix, constants.APIDomain)
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// HostKeyCallback returns an ssh.HostKeyCallback that validates host
|
|
// keys/certs against SSH CAs in the KeyRing.
|
|
//
|
|
// If not CAs are present in the KeyRing, 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 (k *KeyRing) HostKeyCallback(hostnames ...string) (ssh.HostKeyCallback, error) {
|
|
trustedHostKeys, err := k.authorizedHostKeys(hostnames...)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return sshutils.HostKeyCallback(trustedHostKeys, true)
|
|
}
|
|
|
|
// authorizedHostKeys returns all authorized host keys from this KeyRing. If any host
|
|
// names are provided, only matching host keys will be returned.
|
|
func (k *KeyRing) authorizedHostKeys(hostnames ...string) ([]ssh.PublicKey, error) {
|
|
var hostKeys []ssh.PublicKey
|
|
for _, ca := range k.TrustedCerts {
|
|
// Mirror the hosts we would find in a known_hosts entry.
|
|
hosts := []string{k.ProxyHost, ca.ClusterName, "*." + ca.ClusterName}
|
|
|
|
if len(hostnames) == 0 || sshutils.HostNameMatch(hostnames, hosts) {
|
|
for _, authorizedKey := range ca.AuthorizedKeys {
|
|
sshPub, _, _, _, err := ssh.ParseAuthorizedKey(authorizedKey)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
hostKeys = append(hostKeys, sshPub)
|
|
}
|
|
}
|
|
}
|
|
return hostKeys, nil
|
|
}
|
|
|
|
// TeleportClientTLSConfig returns client TLS configuration used
|
|
// to authenticate against API servers.
|
|
func (k *KeyRing) TeleportClientTLSConfig(cipherSuites []uint16, clusters []string) (*tls.Config, error) {
|
|
if len(k.TLSCert) == 0 {
|
|
return nil, trace.NotFound("TLS certificate not found")
|
|
}
|
|
return k.clientTLSConfig(cipherSuites, TLSCredential{
|
|
PrivateKey: k.TLSPrivateKey,
|
|
Cert: k.TLSCert,
|
|
}, clusters)
|
|
}
|
|
|
|
func (k *KeyRing) clientTLSConfig(cipherSuites []uint16, cred TLSCredential, clusters []string) (*tls.Config, error) {
|
|
tlsCert, err := cred.TLSCertificate()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
pool, err := k.clientCertPool(clusters...)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
tlsConfig := utils.TLSConfig(cipherSuites)
|
|
tlsConfig.RootCAs = pool
|
|
tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
|
|
// Use Issuer CN from the certificate to populate the correct SNI in
|
|
// requests.
|
|
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "failed to parse TLS cert")
|
|
}
|
|
tlsConfig.ServerName = apiutils.EncodeClusterName(leaf.Issuer.CommonName)
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// ClientCertPool returns x509.CertPool containing trusted CA.
|
|
func (k *KeyRing) clientCertPool(clusters ...string) (*x509.CertPool, error) {
|
|
certPoolPEM, err := k.clientCertPoolPEM(clusters...)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if len(certPoolPEM) == 0 {
|
|
// It's valid to have no matching CAs and therefore an empty cert pool.
|
|
return pool, nil
|
|
}
|
|
if !pool.AppendCertsFromPEM(certPoolPEM) {
|
|
return nil, trace.BadParameter("failed to parse TLS CA certificate")
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
func (k *KeyRing) clientCertPoolPEM(clusters ...string) ([]byte, error) {
|
|
var certPoolPEM bytes.Buffer
|
|
for _, caPEM := range k.TLSCAs() {
|
|
cert, err := tlsca.ParseCertificatePEM(caPEM)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "parsing TLS CA certificate")
|
|
}
|
|
if !slices.Contains(clusters, cert.Subject.CommonName) {
|
|
continue
|
|
}
|
|
certPoolPEM.Write(caPEM)
|
|
// PEM files should end with a trailing newline, just double check
|
|
// before potentially concatenating multiple together.
|
|
if caPEM[len(caPEM)-1] != '\n' {
|
|
certPoolPEM.WriteByte('\n')
|
|
}
|
|
}
|
|
return certPoolPEM.Bytes(), nil
|
|
}
|
|
|
|
// ProxyClientSSHConfig returns an ssh.ClientConfig with SSH credentials from this
|
|
// KeyRing and HostKeyCallback matching SSH CAs in the KeyRing.
|
|
//
|
|
// The config is set up to authenticate to proxy with the first available principal
|
|
// and ( if keyStore != nil ) trust local SSH CAs without asking for public keys.
|
|
func (k *KeyRing) ProxyClientSSHConfig(hostname string) (*ssh.ClientConfig, error) {
|
|
sshCert, err := k.SSHCert()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "failed to extract username from SSH certificate")
|
|
}
|
|
|
|
sshConfig, err := sshutils.ProxyClientSSHConfig(sshCert, k.SSHPrivateKey.Signer)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
sshConfig.HostKeyCallback, err = k.HostKeyCallback(hostname)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return sshConfig, nil
|
|
}
|
|
|
|
// CertUsername returns the name of the Teleport user encoded in the SSH certificate.
|
|
func (k *KeyRing) CertUsername() (string, error) {
|
|
cert, err := k.SSHCert()
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
return cert.KeyId, nil
|
|
}
|
|
|
|
// CertPrincipals returns the principals listed on the SSH certificate.
|
|
func (k *KeyRing) CertPrincipals() ([]string, error) {
|
|
cert, err := k.SSHCert()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return cert.ValidPrincipals, nil
|
|
}
|
|
|
|
func (k *KeyRing) CertRoles() ([]string, error) {
|
|
cert, err := k.SSHCert()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
ident, err := sshca.DecodeIdentity(cert)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return ident.Roles, nil
|
|
}
|
|
|
|
const (
|
|
agentKeyCommentPrefix = "teleport"
|
|
agentKeyCommentSeparator = ":"
|
|
)
|
|
|
|
// teleportAgentKeyComment returns a teleport agent key comment
|
|
// like "teleport:<proxyHost>:<userName>:<clusterName>".
|
|
func teleportAgentKeyComment(k KeyRingIndex) string {
|
|
return strings.Join([]string{
|
|
agentKeyCommentPrefix,
|
|
k.ProxyHost,
|
|
k.ClusterName,
|
|
k.Username,
|
|
}, agentKeyCommentSeparator)
|
|
}
|
|
|
|
// parseTeleportAgentKeyComment parses an agent key comment into
|
|
// its associated KeyRingIndex.
|
|
func parseTeleportAgentKeyComment(comment string) (KeyRingIndex, bool) {
|
|
parts := strings.Split(comment, agentKeyCommentSeparator)
|
|
if len(parts) != 4 || parts[0] != agentKeyCommentPrefix {
|
|
return KeyRingIndex{}, false
|
|
}
|
|
|
|
return KeyRingIndex{
|
|
ProxyHost: parts[1],
|
|
ClusterName: parts[2],
|
|
Username: parts[3],
|
|
}, true
|
|
}
|
|
|
|
// isTeleportAgentKey returns whether the given agent key was added
|
|
// by Teleport by checking the key's comment.
|
|
func isTeleportAgentKey(key *agent.Key) bool {
|
|
return strings.HasPrefix(key.Comment, agentKeyCommentPrefix+agentKeyCommentSeparator)
|
|
}
|
|
|
|
// AsAgentKey converts client.KeyRing struct to an agent.AddedKey. Any agent.AddedKey
|
|
// can be added to a local agent (keyring), but non-standard keys cannot be added
|
|
// to an SSH system agent through the ssh agent protocol. Check canAddToSystemAgent
|
|
// before adding this key to an SSH system agent.
|
|
func (k *KeyRing) AsAgentKey() (agent.AddedKey, error) {
|
|
sshCert, err := k.SSHCert()
|
|
if err != nil {
|
|
return agent.AddedKey{}, trace.Wrap(err)
|
|
}
|
|
|
|
return agent.AddedKey{
|
|
PrivateKey: k.SSHPrivateKey.Signer,
|
|
Certificate: sshCert,
|
|
Comment: teleportAgentKeyComment(k.KeyRingIndex),
|
|
LifetimeSecs: 0,
|
|
ConfirmBeforeUse: false,
|
|
}, nil
|
|
}
|
|
|
|
// canAddToSystemAgent returns whether this agent key can be added to an SSH system agent.
|
|
// Non-standard private keys will return false.
|
|
func canAddToSystemAgent(agentKey agent.AddedKey) bool {
|
|
switch agentKey.PrivateKey.(type) {
|
|
case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// TeleportTLSCertificate returns the parsed x509 certificate for
|
|
// authentication against Teleport APIs.
|
|
func (k *KeyRing) TeleportTLSCertificate() (*x509.Certificate, error) {
|
|
if len(k.TLSCert) == 0 {
|
|
return nil, trace.NotFound("TLS certificate not found")
|
|
}
|
|
return tlsca.ParseCertificatePEM(k.TLSCert)
|
|
}
|
|
|
|
// KubeX509Cert returns the parsed x509 certificate for authentication against
|
|
// a named kubernetes cluster.
|
|
func (k *KeyRing) KubeX509Cert(kubeClusterName string) (*x509.Certificate, error) {
|
|
cred, ok := k.KubeTLSCredentials[kubeClusterName]
|
|
if !ok {
|
|
return nil, trace.NotFound("TLS credential for kubernetes cluster %q not found", kubeClusterName)
|
|
}
|
|
return tlsca.ParseCertificatePEM(cred.Cert)
|
|
}
|
|
|
|
// KubeTLSCert returns the tls.Certificate for authentication against a named
|
|
// kubernetes cluster.
|
|
func (k *KeyRing) KubeTLSCert(kubeClusterName string) (tls.Certificate, error) {
|
|
cred, ok := k.KubeTLSCredentials[kubeClusterName]
|
|
if !ok {
|
|
return tls.Certificate{}, trace.NotFound("TLS certificate for kubernetes cluster %q not found", kubeClusterName)
|
|
}
|
|
tlsCert, err := cred.TLSCertificate()
|
|
if err != nil {
|
|
return tls.Certificate{}, trace.Wrap(err)
|
|
}
|
|
return tlsCert, nil
|
|
}
|
|
|
|
// DBTLSCert returns the tls.Certificate for authentication against a named database.
|
|
func (k *KeyRing) DBTLSCert(dbName string) (tls.Certificate, error) {
|
|
cred, ok := k.DBTLSCredentials[dbName]
|
|
if !ok {
|
|
return tls.Certificate{}, trace.NotFound("TLS certificate for database %q not found", dbName)
|
|
}
|
|
return cred.TLSCertificate()
|
|
}
|
|
|
|
// DBTLSCertificates returns all parsed x509 database access certificates.
|
|
func (k *KeyRing) DBTLSCertificates() (certs []x509.Certificate, err error) {
|
|
for _, cred := range k.DBTLSCredentials {
|
|
cert, err := tlsca.ParseCertificatePEM(cred.Cert)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
certs = append(certs, *cert)
|
|
}
|
|
return certs, nil
|
|
}
|
|
|
|
// AppTLSCert returns the tls.Certificate for authentication against a named app.
|
|
func (k *KeyRing) AppTLSCert(appName string) (tls.Certificate, error) {
|
|
cred, ok := k.AppTLSCredentials[appName]
|
|
if !ok {
|
|
return tls.Certificate{}, trace.NotFound("TLS certificate for application %q not found", appName)
|
|
}
|
|
return cred.TLSCertificate()
|
|
}
|
|
|
|
// WindowsDesktopTLSCert returns the tls.Certificate for authentication against a named desktop.
|
|
func (k *KeyRing) WindowsDesktopTLSCert(desktopName string) (tls.Certificate, error) {
|
|
cred, ok := k.WindowsDesktopTLSCredentials[desktopName]
|
|
if !ok {
|
|
return tls.Certificate{}, trace.NotFound("TLS certificate for Windows desktop %q not found", desktopName)
|
|
}
|
|
return cred.TLSCertificate()
|
|
}
|
|
|
|
// AppTLSCertificates returns all parsed x509 app access certificates.
|
|
func (k *KeyRing) AppTLSCertificates() (certs []x509.Certificate, err error) {
|
|
for _, cred := range k.AppTLSCredentials {
|
|
cert, err := tlsca.ParseCertificatePEM(cred.Cert)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
certs = append(certs, *cert)
|
|
}
|
|
return certs, nil
|
|
}
|
|
|
|
// TeleportTLSCertValidBefore returns the time of the TLS cert expiration
|
|
func (k *KeyRing) TeleportTLSCertValidBefore() (t time.Time, err error) {
|
|
cert, err := k.TeleportTLSCertificate()
|
|
if err != nil {
|
|
return t, trace.Wrap(err)
|
|
}
|
|
return cert.NotAfter, nil
|
|
}
|
|
|
|
// CertValidBefore returns the time of the cert expiration
|
|
func (k *KeyRing) CertValidBefore() (t time.Time, err error) {
|
|
cert, err := k.SSHCert()
|
|
if err != nil {
|
|
return t, trace.Wrap(err)
|
|
}
|
|
return time.Unix(int64(cert.ValidBefore), 0), 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 (k *KeyRing) AsAuthMethod() (ssh.AuthMethod, error) {
|
|
cert, err := k.SSHCert()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return sshutils.AsAuthMethod(cert, k.SSHPrivateKey)
|
|
}
|
|
|
|
// SSHSigner returns an ssh.Signer using the SSH certificate in this key.
|
|
func (k *KeyRing) SSHSigner() (ssh.Signer, error) {
|
|
cert, err := k.SSHCert()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return sshutils.SSHSigner(cert, k.SSHPrivateKey)
|
|
}
|
|
|
|
// SSHCert returns parsed SSH certificate
|
|
func (k *KeyRing) SSHCert() (*ssh.Certificate, error) {
|
|
if k.Cert == nil {
|
|
return nil, trace.NotFound("SSH cert not found")
|
|
}
|
|
return sshutils.ParseCertificate(k.Cert)
|
|
}
|
|
|
|
// ActiveRequests gets the active requests associated with this key.
|
|
func (k *KeyRing) ActiveRequests() ([]string, error) {
|
|
sshCert, err := k.SSHCert()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
ident, err := sshca.DecodeIdentity(sshCert)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return ident.ActiveRequests, nil
|
|
}
|
|
|
|
// CheckCert makes sure the key's SSH certificate is valid.
|
|
func (k *KeyRing) CheckCert() error {
|
|
cert, err := k.SSHCert()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if err := k.checkCert(cert); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkCert makes sure the given SSH certificate is valid.
|
|
func (k *KeyRing) checkCert(sshCert *ssh.Certificate) error {
|
|
// Check that the certificate was for the current public key. If not, the
|
|
// public/private key pair may have been rotated.
|
|
if !sshutils.KeysEqual(sshCert.Key, k.SSHPrivateKey.SSHPublicKey()) {
|
|
return trace.CompareFailed("public key in profile does not match the public key in SSH certificate")
|
|
}
|
|
|
|
// A valid principal is always passed in because the principals are not being
|
|
// checked here, but rather the validity period, signature, and algorithms.
|
|
certChecker := sshutils.CertChecker{
|
|
FIPS: isFIPS(),
|
|
}
|
|
if len(sshCert.ValidPrincipals) == 0 {
|
|
return trace.BadParameter("cert is not valid for any principles")
|
|
}
|
|
if err := certChecker.CheckCert(sshCert.ValidPrincipals[0], sshCert); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RootClusterName extracts the root cluster name from the issuer
|
|
// of the Teleport TLS certificate.
|
|
func (k *KeyRing) RootClusterName() (string, error) {
|
|
cert, err := k.TeleportTLSCertificate()
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
clusterName := cert.Issuer.CommonName
|
|
if clusterName == "" {
|
|
return "", trace.NotFound("failed to extract root cluster name from Teleport TLS cert")
|
|
}
|
|
return clusterName, nil
|
|
}
|
|
|
|
// EqualPrivateKey returns whether this key and the given key have the same PrivateKey.
|
|
func (k *KeyRing) EqualPrivateKey(other *KeyRing) bool {
|
|
// Compare both private and public key PEM, since hardware keys
|
|
// may not be uniquely identifiable by their private key PEM alone.
|
|
// For example, for PIV keys, the private key PEM only uniquely
|
|
// identifies a PIV slot, so we can use the public key to verify
|
|
// that the private key on the slot hasn't changed.
|
|
return bytes.Equal(k.SSHPrivateKey.MarshalSSHPublicKey(), other.SSHPrivateKey.MarshalSSHPublicKey()) &&
|
|
bytes.Equal(k.TLSPrivateKey.MarshalSSHPublicKey(), other.TLSPrivateKey.MarshalSSHPublicKey()) &&
|
|
subtle.ConstantTimeCompare(k.SSHPrivateKey.PrivateKeyPEM(), other.SSHPrivateKey.PrivateKeyPEM()) == 1 &&
|
|
subtle.ConstantTimeCompare(k.TLSPrivateKey.PrivateKeyPEM(), other.TLSPrivateKey.PrivateKeyPEM()) == 1
|
|
}
|