diff --git a/models/ssh_key.go b/models/ssh_key.go index cbd68a307ef..b2a905305ac 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -7,8 +7,12 @@ package models import ( "bufio" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" "encoding/base64" "encoding/binary" + "encoding/pem" "errors" "fmt" "io/ioutil" @@ -94,6 +98,8 @@ func extractTypeFromBase64Key(key string) (string, error) { return string(b[4 : 4+keyLength]), nil } +const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----" + // parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253). func parseKeyString(content string) (string, error) { // remove whitespace at start and end @@ -101,7 +107,59 @@ func parseKeyString(content string) (string, error) { var keyType, keyContent, keyComment string - if !strings.Contains(content, "-----BEGIN") { + if content[:len(ssh2keyStart)] == ssh2keyStart { + // Parse SSH2 file format. + + // Transform all legal line endings to a single "\n". + content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content) + + lines := strings.Split(content, "\n") + continuationLine := false + + for _, line := range lines { + // Skip lines that: + // 1) are a continuation of the previous line, + // 2) contain ":" as that are comment lines + // 3) contain "-" as that are begin and end tags + if continuationLine || strings.ContainsAny(line, ":-") { + continuationLine = strings.HasSuffix(line, "\\") + } else { + keyContent += line + } + } + + t, err := extractTypeFromBase64Key(keyContent) + if err != nil { + return "", fmt.Errorf("extractTypeFromBase64Key: %v", err) + } + keyType = t + } else { + if strings.Contains(content, "-----BEGIN") { + // Convert PEM Keys to OpenSSH format + // Transform all legal line endings to a single "\n". + content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content) + + block, _ := pem.Decode([]byte(content)) + if block == nil { + return "", fmt.Errorf("failed to parse PEM block containing the public key") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + var pk rsa.PublicKey + _, err2 := asn1.Unmarshal(block.Bytes, &pk) + if err2 != nil { + return "", fmt.Errorf("failed to parse DER encoded public key as either PKIX or PEM RSA Key: %v %v", err, err2) + } + pub = &pk + } + + sshKey, err := ssh.NewPublicKey(pub) + if err != nil { + return "", fmt.Errorf("unable to convert to ssh public key: %v", err) + } + content = string(ssh.MarshalAuthorizedKey(sshKey)) + } // Parse OpenSSH format. // Remove all newlines @@ -132,32 +190,11 @@ func parseKeyString(content string) (string, error) { } else if keyType != t { return "", fmt.Errorf("key type and content does not match: %s - %s", keyType, t) } - } else { - // Parse SSH2 file format. - - // Transform all legal line endings to a single "\n". - content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content) - - lines := strings.Split(content, "\n") - continuationLine := false - - for _, line := range lines { - // Skip lines that: - // 1) are a continuation of the previous line, - // 2) contain ":" as that are comment lines - // 3) contain "-" as that are begin and end tags - if continuationLine || strings.ContainsAny(line, ":-") { - continuationLine = strings.HasSuffix(line, "\\") - } else { - keyContent += line - } - } - - t, err := extractTypeFromBase64Key(keyContent) - if err != nil { - return "", fmt.Errorf("extractTypeFromBase64Key: %v", err) - } - keyType = t + } + // Finally we need to check whether we can actually read the proposed key: + _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyType + " " + keyContent + " " + keyComment)) + if err != nil { + return "", fmt.Errorf("invalid ssh public key: %v", err) } return keyType + " " + keyContent + " " + keyComment, nil } diff --git a/models/ssh_key_test.go b/models/ssh_key_test.go index 568211a7779..4bb612a6717 100644 --- a/models/ssh_key_test.go +++ b/models/ssh_key_test.go @@ -66,6 +66,67 @@ func Test_CheckPublicKeyString(t *testing.T) { {"ssh-rsa AAAAB3NzaC1yc2EA\r\nAAADAQABAAAAgQDAu7tvI\nvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+\r\nBZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvW\nqIwC4prx/WVk2wLTJjzBAhyNx\r\nfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\r\n\r\n"}, {"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf"}, {"\r\nssh-ed25519 \r\nAAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf\r\n\r\n"}, + {`---- BEGIN SSH2 PUBLIC KEY ---- +Comment: "1024-bit DSA, converted by andrew@phaedra from OpenSSH" +AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3 +ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/ +YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL ++wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8 +A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb +0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgP +aguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxc +Ns4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd6429 +82daopE7zQ/NPAnJfag= +---- END SSH2 PUBLIC KEY ---- +`}, + {`---- BEGIN SSH2 PUBLIC KEY ---- +Comment: "1024-bit RSA, converted by andrew@phaedra from OpenSSH" +AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxB +cQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIV +j0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== +---- END SSH2 PUBLIC KEY ---- +`}, + {`-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAMC7u28i9fpketFe5k1+RHdcsdKy4Ir1mfdfnyXEFxDO6jnFmAHq9HDC +b9C0m4X7Nk+1jmGxAgsEuYX4FnlakpmnWMF5KMfYbuXF632Rtwf6QhWPS08USjIo +j3C9aojALimvH9ZWTbAtMmPMECHI3F8SrsL0J6Jf2lARsSol+QoJAgMBAAE= +-----END RSA PUBLIC KEY----- +`}, + {`-----BEGIN PUBLIC KEY----- +MIIBtzCCASsGByqGSM44BAEwggEeAoGBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn5 +9NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczW +OVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQse +cdKktISwTakzAhUAsyrDtiYTSpS/sMMCxjnC336AJpMCgYBpK7/3xvduajLBD/9v +ASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g ++eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTL +zIyMtkHf/IrPCwlM+pV/M/96YgOBhQACgYEAqQcGn9CKgzgPaguIZooTAOQdvBLM +I5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2 +PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982da +opE7zQ/NPAnJfag= +-----END PUBLIC KEY----- +`}, + {`-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAu7tvIvX6ZHrRXuZNfkR3XLHS +suCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jB +eSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C +9CeiX9pQEbEqJfkKCQIDAQAB +-----END PUBLIC KEY----- +`}, + {`-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzGV4ftTgVMEh/Q+OcE2s +RK0CDfSKAvcZezCiZKr077+juUUfWFvyCvRW3414F7KaWBobAmaNYRTjrFxzJ3zj +karv8TA8eMj7sryqcOC3jxHIOEw4qWgxbsW1jqnPwVGUWXF7uNUAFnwy6yJ8LJbV +mR0nhu4Y4aWnJeBa1b/VdaUujnOUNTccRM087jS0v/HYma05v2AEEP/gfps1iN8x +LReJomY4wJY1ndS0wT71Nt3dvQ3AZphWoXGeONV2bE3gMBsRv0Oo/DYDV4/VsTHl +sMV1do3gF/xAUqWawlZQkNcibME+sQqfE7gZ04hlmDATU2zmbzwuHtFiNv8mVv7O +RQIDAQAB +-----END PUBLIC KEY----- +`}, + {`---- BEGIN SSH2 PUBLIC KEY ---- +Comment: "256-bit ED25519, converted by andrew@phaedra from OpenSSH" +AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf +---- END SSH2 PUBLIC KEY ---- +`}, } { _, err := CheckPublicKeyString(test.content) assert.NoError(t, err)