code.gitea.io/gitea@v1.21.7/models/asymkey/ssh_key_parse.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package asymkey
     5  
     6  import (
     7  	"crypto/rsa"
     8  	"crypto/x509"
     9  	"encoding/asn1"
    10  	"encoding/base64"
    11  	"encoding/binary"
    12  	"encoding/pem"
    13  	"fmt"
    14  	"math/big"
    15  	"os"
    16  	"strconv"
    17  	"strings"
    18  
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/process"
    21  	"code.gitea.io/gitea/modules/setting"
    22  	"code.gitea.io/gitea/modules/util"
    23  
    24  	"golang.org/x/crypto/ssh"
    25  )
    26  
    27  //  ____  __.             __________
    28  // |    |/ _|____ ___.__. \______   \_____ _______  ______ ___________
    29  // |      <_/ __ <   |  |  |     ___/\__  \\_  __ \/  ___// __ \_  __ \
    30  // |    |  \  ___/\___  |  |    |     / __ \|  | \/\___ \\  ___/|  | \/
    31  // |____|__ \___  > ____|  |____|    (____  /__|  /____  >\___  >__|
    32  //         \/   \/\/                      \/           \/     \/
    33  //
    34  // This file contains functions for parsing ssh-keys
    35  //
    36  // TODO: Consider if these functions belong in models - no other models function call them or are called by them
    37  // They may belong in a service or a module
    38  
    39  const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----"
    40  
    41  func extractTypeFromBase64Key(key string) (string, error) {
    42  	b, err := base64.StdEncoding.DecodeString(key)
    43  	if err != nil || len(b) < 4 {
    44  		return "", fmt.Errorf("invalid key format: %w", err)
    45  	}
    46  
    47  	keyLength := int(binary.BigEndian.Uint32(b))
    48  	if len(b) < 4+keyLength {
    49  		return "", fmt.Errorf("invalid key format: not enough length %d", keyLength)
    50  	}
    51  
    52  	return string(b[4 : 4+keyLength]), nil
    53  }
    54  
    55  // parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
    56  func parseKeyString(content string) (string, error) {
    57  	// remove whitespace at start and end
    58  	content = strings.TrimSpace(content)
    59  
    60  	var keyType, keyContent, keyComment string
    61  
    62  	if strings.HasPrefix(content, ssh2keyStart) {
    63  		// Parse SSH2 file format.
    64  
    65  		// Transform all legal line endings to a single "\n".
    66  		content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
    67  
    68  		lines := strings.Split(content, "\n")
    69  		continuationLine := false
    70  
    71  		for _, line := range lines {
    72  			// Skip lines that:
    73  			// 1) are a continuation of the previous line,
    74  			// 2) contain ":" as that are comment lines
    75  			// 3) contain "-" as that are begin and end tags
    76  			if continuationLine || strings.ContainsAny(line, ":-") {
    77  				continuationLine = strings.HasSuffix(line, "\\")
    78  			} else {
    79  				keyContent += line
    80  			}
    81  		}
    82  
    83  		t, err := extractTypeFromBase64Key(keyContent)
    84  		if err != nil {
    85  			return "", fmt.Errorf("extractTypeFromBase64Key: %w", err)
    86  		}
    87  		keyType = t
    88  	} else {
    89  		if strings.Contains(content, "-----BEGIN") {
    90  			// Convert PEM Keys to OpenSSH format
    91  			// Transform all legal line endings to a single "\n".
    92  			content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
    93  
    94  			block, _ := pem.Decode([]byte(content))
    95  			if block == nil {
    96  				return "", fmt.Errorf("failed to parse PEM block containing the public key")
    97  			}
    98  			if strings.Contains(block.Type, "PRIVATE") {
    99  				return "", ErrKeyIsPrivate
   100  			}
   101  
   102  			pub, err := x509.ParsePKIXPublicKey(block.Bytes)
   103  			if err != nil {
   104  				var pk rsa.PublicKey
   105  				_, err2 := asn1.Unmarshal(block.Bytes, &pk)
   106  				if err2 != nil {
   107  					return "", fmt.Errorf("failed to parse DER encoded public key as either PKIX or PEM RSA Key: %v %w", err, err2)
   108  				}
   109  				pub = &pk
   110  			}
   111  
   112  			sshKey, err := ssh.NewPublicKey(pub)
   113  			if err != nil {
   114  				return "", fmt.Errorf("unable to convert to ssh public key: %w", err)
   115  			}
   116  			content = string(ssh.MarshalAuthorizedKey(sshKey))
   117  		}
   118  		// Parse OpenSSH format.
   119  
   120  		// Remove all newlines
   121  		content = strings.NewReplacer("\r\n", "", "\n", "").Replace(content)
   122  
   123  		parts := strings.SplitN(content, " ", 3)
   124  		switch len(parts) {
   125  		case 0:
   126  			return "", util.NewInvalidArgumentErrorf("empty key")
   127  		case 1:
   128  			keyContent = parts[0]
   129  		case 2:
   130  			keyType = parts[0]
   131  			keyContent = parts[1]
   132  		default:
   133  			keyType = parts[0]
   134  			keyContent = parts[1]
   135  			keyComment = parts[2]
   136  		}
   137  
   138  		// If keyType is not given, extract it from content. If given, validate it.
   139  		t, err := extractTypeFromBase64Key(keyContent)
   140  		if err != nil {
   141  			return "", fmt.Errorf("extractTypeFromBase64Key: %w", err)
   142  		}
   143  		if len(keyType) == 0 {
   144  			keyType = t
   145  		} else if keyType != t {
   146  			return "", fmt.Errorf("key type and content does not match: %s - %s", keyType, t)
   147  		}
   148  	}
   149  	// Finally we need to check whether we can actually read the proposed key:
   150  	_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyType + " " + keyContent + " " + keyComment))
   151  	if err != nil {
   152  		return "", fmt.Errorf("invalid ssh public key: %w", err)
   153  	}
   154  	return keyType + " " + keyContent + " " + keyComment, nil
   155  }
   156  
   157  // CheckPublicKeyString checks if the given public key string is recognized by SSH.
   158  // It returns the actual public key line on success.
   159  func CheckPublicKeyString(content string) (_ string, err error) {
   160  	content, err = parseKeyString(content)
   161  	if err != nil {
   162  		return "", err
   163  	}
   164  
   165  	content = strings.TrimRight(content, "\n\r")
   166  	if strings.ContainsAny(content, "\n\r") {
   167  		return "", util.NewInvalidArgumentErrorf("only a single line with a single key please")
   168  	}
   169  
   170  	// remove any unnecessary whitespace now
   171  	content = strings.TrimSpace(content)
   172  
   173  	if !setting.SSH.MinimumKeySizeCheck {
   174  		return content, nil
   175  	}
   176  
   177  	var (
   178  		fnName  string
   179  		keyType string
   180  		length  int
   181  	)
   182  	if len(setting.SSH.KeygenPath) == 0 {
   183  		fnName = "SSHNativeParsePublicKey"
   184  		keyType, length, err = SSHNativeParsePublicKey(content)
   185  	} else {
   186  		fnName = "SSHKeyGenParsePublicKey"
   187  		keyType, length, err = SSHKeyGenParsePublicKey(content)
   188  	}
   189  	if err != nil {
   190  		return "", fmt.Errorf("%s: %w", fnName, err)
   191  	}
   192  	log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length)
   193  
   194  	if minLen, found := setting.SSH.MinimumKeySizes[keyType]; found && length >= minLen {
   195  		return content, nil
   196  	} else if found && length < minLen {
   197  		return "", fmt.Errorf("key length is not enough: got %d, needs %d", length, minLen)
   198  	}
   199  	return "", fmt.Errorf("key type is not allowed: %s", keyType)
   200  }
   201  
   202  // SSHNativeParsePublicKey extracts the key type and length using the golang SSH library.
   203  func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
   204  	fields := strings.Fields(keyLine)
   205  	if len(fields) < 2 {
   206  		return "", 0, fmt.Errorf("not enough fields in public key line: %s", keyLine)
   207  	}
   208  
   209  	raw, err := base64.StdEncoding.DecodeString(fields[1])
   210  	if err != nil {
   211  		return "", 0, err
   212  	}
   213  
   214  	pkey, err := ssh.ParsePublicKey(raw)
   215  	if err != nil {
   216  		if strings.Contains(err.Error(), "ssh: unknown key algorithm") {
   217  			return "", 0, ErrKeyUnableVerify{err.Error()}
   218  		}
   219  		return "", 0, fmt.Errorf("ParsePublicKey: %w", err)
   220  	}
   221  
   222  	// The ssh library can parse the key, so next we find out what key exactly we have.
   223  	switch pkey.Type() {
   224  	case ssh.KeyAlgoDSA:
   225  		rawPub := struct {
   226  			Name       string
   227  			P, Q, G, Y *big.Int
   228  		}{}
   229  		if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
   230  			return "", 0, err
   231  		}
   232  		// as per https://bugzilla.mindrot.org/show_bug.cgi?id=1647 we should never
   233  		// see dsa keys != 1024 bit, but as it seems to work, we will not check here
   234  		return "dsa", rawPub.P.BitLen(), nil // use P as per crypto/dsa/dsa.go (is L)
   235  	case ssh.KeyAlgoRSA:
   236  		rawPub := struct {
   237  			Name string
   238  			E    *big.Int
   239  			N    *big.Int
   240  		}{}
   241  		if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
   242  			return "", 0, err
   243  		}
   244  		return "rsa", rawPub.N.BitLen(), nil // use N as per crypto/rsa/rsa.go (is bits)
   245  	case ssh.KeyAlgoECDSA256:
   246  		return "ecdsa", 256, nil
   247  	case ssh.KeyAlgoECDSA384:
   248  		return "ecdsa", 384, nil
   249  	case ssh.KeyAlgoECDSA521:
   250  		return "ecdsa", 521, nil
   251  	case ssh.KeyAlgoED25519:
   252  		return "ed25519", 256, nil
   253  	case ssh.KeyAlgoSKECDSA256:
   254  		return "ecdsa-sk", 256, nil
   255  	case ssh.KeyAlgoSKED25519:
   256  		return "ed25519-sk", 256, nil
   257  	}
   258  	return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type())
   259  }
   260  
   261  // writeTmpKeyFile writes key content to a temporary file
   262  // and returns the name of that file, along with any possible errors.
   263  func writeTmpKeyFile(content string) (string, error) {
   264  	tmpFile, err := os.CreateTemp(setting.SSH.KeyTestPath, "gitea_keytest")
   265  	if err != nil {
   266  		return "", fmt.Errorf("TempFile: %w", err)
   267  	}
   268  	defer tmpFile.Close()
   269  
   270  	if _, err = tmpFile.WriteString(content); err != nil {
   271  		return "", fmt.Errorf("WriteString: %w", err)
   272  	}
   273  	return tmpFile.Name(), nil
   274  }
   275  
   276  // SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen.
   277  func SSHKeyGenParsePublicKey(key string) (string, int, error) {
   278  	tmpName, err := writeTmpKeyFile(key)
   279  	if err != nil {
   280  		return "", 0, fmt.Errorf("writeTmpKeyFile: %w", err)
   281  	}
   282  	defer func() {
   283  		if err := util.Remove(tmpName); err != nil {
   284  			log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpName, err)
   285  		}
   286  	}()
   287  
   288  	keygenPath := setting.SSH.KeygenPath
   289  	if len(keygenPath) == 0 {
   290  		keygenPath = "ssh-keygen"
   291  	}
   292  
   293  	stdout, stderr, err := process.GetManager().Exec("SSHKeyGenParsePublicKey", keygenPath, "-lf", tmpName)
   294  	if err != nil {
   295  		return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
   296  	}
   297  	if strings.Contains(stdout, "is not a public key file") {
   298  		return "", 0, ErrKeyUnableVerify{stdout}
   299  	}
   300  
   301  	fields := strings.Split(stdout, " ")
   302  	if len(fields) < 4 {
   303  		return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
   304  	}
   305  
   306  	keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
   307  	length, err := strconv.ParseInt(fields[0], 10, 32)
   308  	if err != nil {
   309  		return "", 0, err
   310  	}
   311  	return strings.ToLower(keyType), int(length), nil
   312  }