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 }