github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/identityfile/identityfile.go (about)

     1  /*
     2  Copyright 2021 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package identityfile implements parsing and serialization of Teleport identity files.
    18  package identityfile
    19  
    20  import (
    21  	"bufio"
    22  	"bytes"
    23  	"crypto/tls"
    24  	"crypto/x509"
    25  	"fmt"
    26  	"io"
    27  	"os"
    28  	"strings"
    29  
    30  	"github.com/gravitational/trace"
    31  	"golang.org/x/crypto/ssh"
    32  
    33  	"github.com/gravitational/teleport/api/utils/keypaths"
    34  	"github.com/gravitational/teleport/api/utils/keys"
    35  	"github.com/gravitational/teleport/api/utils/sshutils"
    36  )
    37  
    38  const (
    39  	// FilePermissions defines file permissions for identity files.
    40  	//
    41  	// Specifically, for postgres, this must be 0600 or 0640 (choosing 0600 as it's more restrictive)
    42  	// https://www.postgresql.org/docs/current/libpq-ssl.html
    43  	// On Unix systems, the permissions on the private key file must disallow any access to world or group;
    44  	//  achieve this by a command such as chmod 0600 ~/.postgresql/postgresql.key.
    45  	// Alternatively, the file can be owned by root and have group read access (that is, 0640 permissions).
    46  	//
    47  	// Other services should accept 0600 as well, if not, we must change the Write function (in `lib/client/identityfile/identity.go`)
    48  	FilePermissions = 0600
    49  )
    50  
    51  // IdentityFile represents the basic components of an identity file.
    52  type IdentityFile struct {
    53  	// PrivateKey is PEM encoded private key data.
    54  	PrivateKey []byte
    55  	// Certs contains PEM encoded certificates.
    56  	Certs Certs
    57  	// CACerts contains PEM encoded CA certificates.
    58  	CACerts CACerts
    59  }
    60  
    61  // Certs contains PEM encoded certificates.
    62  type Certs struct {
    63  	// SSH is a cert used for SSH.
    64  	SSH []byte
    65  	// TLS is a cert used for TLS.
    66  	TLS []byte
    67  }
    68  
    69  // CACerts contains PEM encoded CA certificates.
    70  type CACerts struct {
    71  	// SSH are CA certs used for SSH in known_hosts format.
    72  	SSH [][]byte
    73  	// TLS are CA certs used for TLS.
    74  	TLS [][]byte
    75  }
    76  
    77  // TLSConfig returns the identity file's associated TLSConfig.
    78  func (i *IdentityFile) TLSConfig() (*tls.Config, error) {
    79  	cert, err := keys.X509KeyPair(i.Certs.TLS, i.PrivateKey)
    80  	if err != nil {
    81  		return nil, trace.Wrap(err)
    82  	}
    83  
    84  	pool := x509.NewCertPool()
    85  	for _, caCerts := range i.CACerts.TLS {
    86  		if !pool.AppendCertsFromPEM(caCerts) {
    87  			return nil, trace.BadParameter("invalid CA cert PEM")
    88  		}
    89  	}
    90  
    91  	return &tls.Config{
    92  		Certificates: []tls.Certificate{cert},
    93  		RootCAs:      pool,
    94  	}, nil
    95  }
    96  
    97  // SSHClientConfig returns the identity file's associated SSHClientConfig.
    98  func (i *IdentityFile) SSHClientConfig() (*ssh.ClientConfig, error) {
    99  	sshCert, err := sshutils.ParseCertificate(i.Certs.SSH)
   100  	if err != nil {
   101  		return nil, trace.Wrap(err)
   102  	}
   103  
   104  	priv, err := keys.ParsePrivateKey(i.PrivateKey)
   105  	if err != nil {
   106  		return nil, trace.Wrap(err)
   107  	}
   108  
   109  	ssh, err := sshutils.ProxyClientSSHConfig(sshCert, priv, i.CACerts.SSH...)
   110  	if err != nil {
   111  		return nil, trace.Wrap(err)
   112  	}
   113  
   114  	return ssh, nil
   115  }
   116  
   117  // Write writes the given identityFile to the specified path.
   118  func Write(idFile *IdentityFile, path string) error {
   119  	buf := new(bytes.Buffer)
   120  	if err := encodeIdentityFile(buf, idFile); err != nil {
   121  		return trace.Wrap(err)
   122  	}
   123  	if err := os.WriteFile(path, buf.Bytes(), FilePermissions); err != nil {
   124  		return trace.ConvertSystemError(err)
   125  	}
   126  	return nil
   127  }
   128  
   129  // Encode encodes the given identityFile to bytes.
   130  func Encode(idFile *IdentityFile) ([]byte, error) {
   131  	buf := new(bytes.Buffer)
   132  	if err := encodeIdentityFile(buf, idFile); err != nil {
   133  		return nil, trace.Wrap(err)
   134  	}
   135  
   136  	return buf.Bytes(), nil
   137  }
   138  
   139  // Read reads an identity file from generic io.Reader interface.
   140  func Read(r io.Reader) (*IdentityFile, error) {
   141  	ident, err := decodeIdentityFile(r)
   142  	if err != nil {
   143  		return nil, trace.Wrap(err)
   144  	}
   145  
   146  	if len(ident.Certs.SSH) == 0 {
   147  		return nil, trace.BadParameter("could not find SSH cert in the identity file")
   148  	}
   149  
   150  	return ident, nil
   151  }
   152  
   153  // ReadFile reads an identity file from a given path.
   154  func ReadFile(path string) (*IdentityFile, error) {
   155  	r, err := os.Open(path)
   156  	if err != nil {
   157  		return nil, trace.Wrap(err)
   158  	}
   159  	defer r.Close()
   160  
   161  	ident, err := decodeIdentityFile(r)
   162  	if err != nil {
   163  		return nil, trace.Wrap(err)
   164  	}
   165  
   166  	// Did not find the SSH certificate in the file? look in a
   167  	// separate file with -cert.pub suffix.
   168  	if len(ident.Certs.SSH) == 0 {
   169  		certFn := keypaths.IdentitySSHCertPath(path)
   170  		if ident.Certs.SSH, err = os.ReadFile(certFn); err != nil {
   171  			return nil, trace.Wrap(err, "could not find SSH cert in the identity file or %v", certFn)
   172  		}
   173  	}
   174  
   175  	return ident, nil
   176  }
   177  
   178  // FromString reads an identity file from a string.
   179  func FromString(content string) (*IdentityFile, error) {
   180  	ident, err := decodeIdentityFile(strings.NewReader(content))
   181  	if err != nil {
   182  		return nil, trace.Wrap(err)
   183  	}
   184  
   185  	if len(ident.Certs.SSH) == 0 {
   186  		return nil, trace.BadParameter("could not find SSH cert in the identity file")
   187  	}
   188  
   189  	return ident, nil
   190  }
   191  
   192  // encodeIdentityFile combines the components of an identity file in its file format.
   193  func encodeIdentityFile(w io.Writer, idFile *IdentityFile) error {
   194  	// write key:
   195  	if err := writeWithNewline(w, idFile.PrivateKey); err != nil {
   196  		return trace.Wrap(err)
   197  	}
   198  	// append ssh cert:
   199  	if err := writeWithNewline(w, idFile.Certs.SSH); err != nil {
   200  		return trace.Wrap(err)
   201  	}
   202  	// append tls cert:
   203  	if err := writeWithNewline(w, idFile.Certs.TLS); err != nil {
   204  		return trace.Wrap(err)
   205  	}
   206  	// append ssh ca certificates
   207  	for _, caCert := range idFile.CACerts.SSH {
   208  		if err := writeWithNewline(w, caCert); err != nil {
   209  			return trace.Wrap(err)
   210  		}
   211  	}
   212  	// append tls ca certificates
   213  	for _, caCert := range idFile.CACerts.TLS {
   214  		if err := writeWithNewline(w, caCert); err != nil {
   215  			return trace.Wrap(err)
   216  		}
   217  	}
   218  
   219  	return nil
   220  }
   221  
   222  func writeWithNewline(w io.Writer, data []byte) error {
   223  	if _, err := w.Write(data); err != nil {
   224  		return trace.Wrap(err)
   225  	}
   226  	if bytes.HasSuffix(data, []byte{'\n'}) {
   227  		return nil
   228  	}
   229  	_, err := fmt.Fprintln(w)
   230  	return trace.Wrap(err)
   231  }
   232  
   233  // decodeIdentityFile attempts to break up the contents of an identity file into its
   234  // respective components.
   235  func decodeIdentityFile(idFile io.Reader) (*IdentityFile, error) {
   236  	scanner := bufio.NewScanner(idFile)
   237  	var ident IdentityFile
   238  	// Subslice of scanner's buffer pointing to current line
   239  	// with leading and trailing whitespace trimmed.
   240  	var line []byte
   241  	// Attempt to scan to the next line.
   242  	scanln := func() bool {
   243  		if !scanner.Scan() {
   244  			line = nil
   245  			return false
   246  		}
   247  		line = bytes.TrimSpace(scanner.Bytes())
   248  		return true
   249  	}
   250  	// Check if the current line starts with prefix `p`.
   251  	hasPrefix := func(p string) bool {
   252  		return bytes.HasPrefix(line, []byte(p))
   253  	}
   254  	// Get an "owned" copy of the current line.
   255  	cloneln := func() []byte {
   256  		ln := make([]byte, len(line))
   257  		copy(ln, line)
   258  		return ln
   259  	}
   260  	// Scan through all lines of identity file.  Lines with a known prefix
   261  	// are copied out of the scanner's buffer.  All others are ignored.
   262  	for scanln() {
   263  		switch {
   264  		case isSSHCert(line):
   265  			ident.Certs.SSH = append(cloneln(), '\n')
   266  		case hasPrefix("@cert-authority"):
   267  			ident.CACerts.SSH = append(ident.CACerts.SSH, append(cloneln(), '\n'))
   268  		case hasPrefix("-----BEGIN"):
   269  			// Current line marks the beginning of a PEM block.  Consume all
   270  			// lines until a corresponding END is found.
   271  			var pemBlock []byte
   272  			for {
   273  				pemBlock = append(pemBlock, line...)
   274  				pemBlock = append(pemBlock, '\n')
   275  				if hasPrefix("-----END") {
   276  					break
   277  				}
   278  				if !scanln() {
   279  					// If scanner has terminated in the middle of a PEM block, either
   280  					// the reader encountered an error, or the PEM block is a fragment.
   281  					if err := scanner.Err(); err != nil {
   282  						return nil, trace.Wrap(err)
   283  					}
   284  					return nil, trace.BadParameter("invalid PEM block (fragment)")
   285  				}
   286  			}
   287  			// Decide where to place the pem block based on
   288  			// which pem blocks have already been found.
   289  			switch {
   290  			case ident.PrivateKey == nil:
   291  				ident.PrivateKey = pemBlock
   292  			case ident.Certs.TLS == nil:
   293  				ident.Certs.TLS = pemBlock
   294  			default:
   295  				ident.CACerts.TLS = append(ident.CACerts.TLS, pemBlock)
   296  			}
   297  		}
   298  	}
   299  	if err := scanner.Err(); err != nil {
   300  		return nil, trace.Wrap(err)
   301  	}
   302  	return &ident, nil
   303  }
   304  
   305  // Check if the given data has an ssh cert type prefix as it's first part.
   306  func isSSHCert(data []byte) bool {
   307  	sshCertType := bytes.Split(data, []byte(" "))[0]
   308  	return sshutils.IsSSHCertType(string(sshCertType))
   309  }