| // Copyright 2022 Google LLC | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package ssh | |
| import ( | |
| "crypto/rand" | |
| "crypto/rsa" | |
| "encoding/base64" | |
| "fmt" | |
| "slices" | |
| "testing" | |
| "golang.org/x/crypto/ssh" | |
| // allowlist:crypto/rsa | |
| ) | |
| func TestGetUserKey(t *testing.T) { | |
| pubKey := MakeRandRSAPubKey(t) | |
| table := []struct { | |
| key string | |
| user string | |
| keyVal string | |
| haserr bool | |
| }{ | |
| {fmt.Sprintf(`usera:ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), "usera", fmt.Sprintf(`ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), false}, | |
| {fmt.Sprintf(`usera:restrict,pty ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), "usera", fmt.Sprintf(`restrict,pty ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), false}, | |
| {" ", "", "", true}, | |
| {fmt.Sprintf("ssh-rsa %s", pubKey), "", "", true}, | |
| {fmt.Sprintf(":ssh-rsa %s", pubKey), "", "", true}, | |
| {"userb:", "", "", true}, | |
| {fmt.Sprintf("userc:ssh-rsa %s info text", pubKey), "userc", fmt.Sprintf("ssh-rsa %s info text", pubKey), false}, | |
| } | |
| for _, tt := range table { | |
| u, k, err := GetUserKey(tt.key) | |
| e := err != nil | |
| if u != tt.user || k != tt.keyVal || e != tt.haserr { | |
| t.Errorf("GetUserKey(%s) incorrect return: got user: %s, key: %s, error: %v - want user: %s, key: %s, error: %v", tt.key, u, k, e, tt.user, tt.keyVal, tt.haserr) | |
| } | |
| } | |
| } | |
| func TestValidateUserKey(t *testing.T) { | |
| pubKey := MakeRandRSAPubKey(t) | |
| table := []struct { | |
| user string | |
| key string | |
| haserr bool | |
| }{ | |
| {"usera", fmt.Sprintf(`ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), false}, | |
| {"user a", fmt.Sprintf(`ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), true}, | |
| {"usera", fmt.Sprintf(`ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2021-04-23T12:34:56+0000"}`, pubKey), true}, | |
| {"usera", fmt.Sprintf(`ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"Apri 4, 2056"}`, pubKey), true}, | |
| {"usera", fmt.Sprintf(`ssh-rsa %s google-ssh`, pubKey), true}, | |
| {"usera", fmt.Sprintf(`ssh-rsa %s test info`, pubKey), false}, | |
| {"", fmt.Sprintf("ssh-rsa %s", pubKey), true}, | |
| // Ignore safetext/shsprintf linter suggestion. | |
| {"usera", fmt.Sprintf(`command="echo hi" ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), false}, | |
| {"usera", fmt.Sprintf(`command="echo hi" ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2021-04-23T12:34:56+0000"}`, pubKey), true}, | |
| {"usera", fmt.Sprintf(`restrict,pty ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), false}, | |
| {" ", "", true}, | |
| {"userb", "", true}, | |
| } | |
| for _, tt := range table { | |
| err := ValidateUserKey(tt.user, tt.key) | |
| e := err != nil | |
| if e != tt.haserr { | |
| t.Errorf("ValidateUserKey(%s, %s) incorrect return: expected: %t - got: %t", tt.user, tt.key, tt.haserr, e) | |
| } | |
| } | |
| } | |
| func TestCheckExpiredKey(t *testing.T) { | |
| pubKey := MakeRandRSAPubKey(t) | |
| table := []struct { | |
| key string | |
| expired bool | |
| }{ | |
| {fmt.Sprintf(`usera:ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), false}, | |
| {fmt.Sprintf(`usera:ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2021-04-23T12:34:56+0000"}`, pubKey), true}, | |
| {fmt.Sprintf(`usera:ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"Apri 4, 2056"}`, pubKey), true}, | |
| {fmt.Sprintf(`usera:ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":1234}`, pubKey), true}, | |
| {fmt.Sprintf(`usera:ssh-rsa %s google-ssh`, pubKey), true}, | |
| {" ", true}, | |
| {fmt.Sprintf("ssh-rsa %s", pubKey), false}, | |
| {fmt.Sprintf(":ssh-rsa %s", pubKey), false}, | |
| {fmt.Sprintf("usera:ssh-rsa %s", pubKey), false}, | |
| } | |
| for _, tt := range table { | |
| err := CheckExpiredKey(tt.key) | |
| isExpired := err != nil | |
| if isExpired != tt.expired { | |
| t.Errorf("CheckExpiredKey(%s) incorrect return: expired: %t - want expired: %t, got err: %v", tt.key, isExpired, tt.expired, err) | |
| } | |
| } | |
| } | |
| func TestValidateUser(t *testing.T) { | |
| table := []struct { | |
| user string | |
| valid bool | |
| }{ | |
| {"username", true}, | |
| {"username:key", true}, | |
| {"user -g", false}, | |
| {"user -g 27", false}, | |
| {"user\t-g", false}, | |
| {"user\n-g", false}, | |
| {"username\t-g\n27", false}, | |
| {"user\u00a0-g", false}, | |
| {"user\u2000-g", false}, | |
| {"user\u2009-g", false}, | |
| {"user\u202f-g", false}, | |
| {"user\u3000-g", false}, | |
| } | |
| for _, tt := range table { | |
| err := ValidateUser(tt.user) | |
| isValid := err == nil | |
| if isValid != tt.valid { | |
| t.Errorf("ValidateUser(%s) incorrect return: expected: %t - got: %t", tt.user, tt.valid, isValid) | |
| } | |
| } | |
| } | |
| // MakeRandRSAPubKey generates base64 encoded 2048 bit RSA public key for use in tests. | |
| func MakeRandRSAPubKey(t *testing.T) string { | |
| t.Helper() | |
| prv, err := rsa.GenerateKey(rand.Reader, 2048) | |
| if err != nil { | |
| t.Fatalf("error generating RSA key: %v", err) | |
| } | |
| sshPublic, err := ssh.NewPublicKey(prv.Public()) | |
| if err != nil { | |
| t.Fatalf("error wrapping ssh public key: %v", err) | |
| } | |
| return base64.StdEncoding.EncodeToString(sshPublic.Marshal()) | |
| } | |
| func TestParseSSHKeys(t *testing.T) { | |
| pubKeyA := MakeRandRSAPubKey(t) | |
| pubKeyB := MakeRandRSAPubKey(t) | |
| pubKey := MakeRandRSAPubKey(t) | |
| keys := []string{ | |
| "# Here is some random data in the file.", | |
| fmt.Sprint("invalid:"), | |
| fmt.Sprintf("usera:ssh-rsa %s", pubKeyA), | |
| fmt.Sprintf("userb:ssh-rsa %s", pubKeyB), | |
| fmt.Sprintf(`usera:ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), | |
| fmt.Sprintf(`usera:ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2020-04-23T12:34:56+0000"}`, pubKey), | |
| } | |
| expected := []string{ | |
| fmt.Sprintf("ssh-rsa %s", pubKeyA), | |
| fmt.Sprintf(`ssh-rsa %s google-ssh {"userName":"usera@example.com","expireOn":"2095-04-23T12:34:56+0000"}`, pubKey), | |
| } | |
| user := "usera" | |
| if got, want := ParseKeys(user, keys), expected; !slices.Equal(got, want) { | |
| t.Errorf("ParseSSHKeys(%s,%s) incorrect return: got %v, want %v", user, keys, got, want) | |
| } | |
| } |