-
-
Notifications
You must be signed in to change notification settings - Fork 314
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add authentication-key-request-url option (#247)
* Add authentication-key-request-url option to allow validation of ssh public key auth via an http POST request to a separate application * Switch to using JSON body in request and include username & remote address of client.
- Loading branch information
Showing
4 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
package utils | ||
|
||
import ( | ||
"bytes" | ||
"crypto/rand" | ||
"crypto/rsa" | ||
"encoding/json" | ||
"io" | ||
"log" | ||
"net" | ||
"net/http" | ||
"net/http/httptest" | ||
"os" | ||
"testing" | ||
|
||
"github.com/spf13/viper" | ||
"golang.org/x/crypto/ssh" | ||
) | ||
|
||
// MakeTestKeys returns a slice of randomly generated private keys. | ||
func MakeTestKeys(numKeys int) []*rsa.PrivateKey { | ||
testKeys := make([]*rsa.PrivateKey, numKeys) | ||
for i := 0; i < numKeys; i++ { | ||
key, err := rsa.GenerateKey(rand.Reader, 2048) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
testKeys[i] = key | ||
} | ||
return testKeys | ||
} | ||
|
||
type AuthRequestBody struct { | ||
PubKey string `json:"auth_key"` | ||
UserName string `json:"user"` | ||
RemoteAddr string `json:"remote_addr"` | ||
} | ||
|
||
// PubKeyHttpHandler returns a http handler function which validates an | ||
// OpenSSH authorized-keys formatted public key against a slice of | ||
// slice authorized keys. | ||
func PubKeyHttpHandler(validPublicKeys *[]rsa.PublicKey, validUsernames *[]string) func(w http.ResponseWriter, r *http.Request) { | ||
return func(w http.ResponseWriter, r *http.Request) { | ||
pubKey, err := io.ReadAll(r.Body) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusBadRequest) | ||
return | ||
} | ||
var reqBody AuthRequestBody | ||
err = json.Unmarshal(pubKey, &reqBody) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusBadRequest) | ||
return | ||
} | ||
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(reqBody.PubKey)) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusBadRequest) | ||
return | ||
} | ||
marshalled := parsedKey.Marshal() | ||
keyMatch := false | ||
usernameMatch := false | ||
for _, key := range *validPublicKeys { | ||
authorizedKey, err := ssh.NewPublicKey(&key) | ||
if err != nil { | ||
log.Print("Error parsing authorized public key", err) | ||
continue | ||
} | ||
if bytes.Equal(authorizedKey.Marshal(), marshalled) { | ||
keyMatch = true | ||
break | ||
} | ||
} | ||
for _, username := range *validUsernames { | ||
if reqBody.UserName == username { | ||
usernameMatch = true | ||
} | ||
} | ||
if keyMatch && usernameMatch { | ||
w.WriteHeader(http.StatusOK) | ||
} else { | ||
w.WriteHeader(http.StatusUnauthorized) | ||
} | ||
} | ||
} | ||
|
||
// HandleSSHConn accepts an incoming client connection, performs the | ||
// auth handshake to test the GetSSHConfig method using the | ||
// authentication-key-request-url flag. | ||
func HandleSSHConn(sshListener net.Listener, successAuth *chan bool) { | ||
conn, err := sshListener.Accept() | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer conn.Close() | ||
|
||
// GetSSHConfig is the method we are testing to validate that it | ||
// can use an http request to validate client public key auth | ||
connection, _, _, err := ssh.NewServerConn(conn, GetSSHConfig()) | ||
|
||
if err != nil { | ||
*successAuth <- false | ||
return | ||
} | ||
connection.Close() | ||
|
||
*successAuth <- true | ||
} | ||
|
||
// TestAuthenticationKeyRequest validates that the utils.GetSSHConfig | ||
// PublicKey auth works with the authentication-key-request-url parameter. | ||
func TestAuthenticationKeyRequest(t *testing.T) { | ||
testKeys := MakeTestKeys(3) | ||
|
||
// Give sish a temp directory to generate a server ssh host key | ||
dir, err := os.MkdirTemp("", "sish_keys") | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer os.RemoveAll(dir) | ||
viper.Set("private-keys-directory", dir) | ||
viper.Set("authentication", true) | ||
|
||
testCases := []struct { | ||
clientPrivateKey *rsa.PrivateKey | ||
clientUser string | ||
validPublicKeys []rsa.PublicKey | ||
validUsernames []string | ||
expectSuccessAuth bool | ||
overrideHttpUrl string | ||
}{ | ||
// valid key, should succeed auth | ||
{ | ||
clientPrivateKey: testKeys[0], | ||
clientUser: "ubuntu", | ||
validPublicKeys: []rsa.PublicKey{testKeys[0].PublicKey}, | ||
validUsernames: []string{"ubuntu"}, | ||
expectSuccessAuth: true, | ||
overrideHttpUrl: "", | ||
}, | ||
// invalid key, should be rejected | ||
{ | ||
clientPrivateKey: testKeys[0], | ||
clientUser: "ubuntu", | ||
validPublicKeys: []rsa.PublicKey{testKeys[1].PublicKey, testKeys[2].PublicKey}, | ||
validUsernames: []string{"ubuntu"}, | ||
expectSuccessAuth: false, | ||
overrideHttpUrl: "", | ||
}, | ||
// invalid username, should be rejected | ||
{ | ||
clientPrivateKey: testKeys[0], | ||
clientUser: "windows", | ||
validPublicKeys: []rsa.PublicKey{testKeys[0].PublicKey}, | ||
validUsernames: []string{"ubuntu"}, | ||
expectSuccessAuth: false, | ||
overrideHttpUrl: "", | ||
}, | ||
// no http service listening on server url, should be rejected | ||
{ | ||
clientPrivateKey: testKeys[0], | ||
clientUser: "ubuntu", | ||
validPublicKeys: []rsa.PublicKey{testKeys[0].PublicKey}, | ||
validUsernames: []string{"ubuntu"}, | ||
expectSuccessAuth: false, | ||
overrideHttpUrl: "http://localhost:61234", | ||
}, | ||
// invalid http url, should be rejected | ||
{ | ||
clientPrivateKey: testKeys[0], | ||
clientUser: "ubuntu", | ||
validPublicKeys: []rsa.PublicKey{testKeys[0].PublicKey}, | ||
validUsernames: []string{"ubuntu"}, | ||
expectSuccessAuth: false, | ||
overrideHttpUrl: "notarealurl", | ||
}, | ||
} | ||
|
||
for caseIdx, c := range testCases { | ||
if c.overrideHttpUrl == "" { | ||
// start an http server that will validate against the specified public keys | ||
httpSrv := httptest.NewServer(http.HandlerFunc(PubKeyHttpHandler(&c.validPublicKeys, &c.validUsernames))) | ||
defer httpSrv.Close() | ||
|
||
// set viper to this http server URL as the auth request url it will | ||
// send public keys to for auth validation | ||
viper.Set("authentication-key-request-url", httpSrv.URL) | ||
} else { | ||
viper.Set("authentication-key-request-url", c.overrideHttpUrl) | ||
} | ||
|
||
sshListener, err := net.Listen("tcp", "localhost:0") | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
defer sshListener.Close() | ||
|
||
successAuth := make(chan bool) | ||
go HandleSSHConn(sshListener, &successAuth) | ||
|
||
// attempt to connect to the ssh server using the specified private key | ||
signer, err := ssh.NewSignerFromKey(c.clientPrivateKey) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
clientConfig := &ssh.ClientConfig{ | ||
Auth: []ssh.AuthMethod{ | ||
ssh.PublicKeys(signer), | ||
}, | ||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), | ||
User: c.clientUser, | ||
} | ||
t.Log(clientConfig) | ||
|
||
client, err := ssh.Dial("tcp", sshListener.Addr().String(), clientConfig) | ||
if err != nil { | ||
t.Log("ssh client rejected", err) | ||
} else { | ||
t.Log("ssh client connected") | ||
client.Close() | ||
} | ||
|
||
didAuth := <-successAuth | ||
|
||
if didAuth != c.expectSuccessAuth { | ||
t.Errorf("Auth %t when should have been %t for case %d", didAuth, c.expectSuccessAuth, caseIdx) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters