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 }