mirror of
https://github.com/Luzifer/cloudkeys-go.git
synced 2024-11-13 00:12:43 +00:00
Knut Ahlers
a1df72edc5
commitf0db1ff1f8
Author: Knut Ahlers <knut@ahlers.me> Date: Sun Dec 24 12:19:56 2017 +0100 Mark option as deprecated Signed-off-by: Knut Ahlers <knut@ahlers.me> commit9891df2a16
Author: Knut Ahlers <knut@ahlers.me> Date: Sun Dec 24 12:11:56 2017 +0100 Fix: Typo Signed-off-by: Knut Ahlers <knut@ahlers.me> commit836006de64
Author: Knut Ahlers <knut@ahlers.me> Date: Sun Dec 24 12:04:20 2017 +0100 Add new dependencies Signed-off-by: Knut Ahlers <knut@ahlers.me> commitd64fee60c8
Author: Knut Ahlers <knut@ahlers.me> Date: Sun Dec 24 11:55:52 2017 +0100 Replace insecure password hashing Prior this commit passwords were hashed with a static salt and using the SHA1 hashing function. This could lead to passwords being attackable in case someone gets access to the raw data stored inside the database. This commit introduces password hashing using bcrypt hashing function which addresses this issue. Old passwords are not automatically re-hashed as they are unknown. Replacing the old password scheme is not that easy and needs #10 to be solved. Therefore the old hashing scheme is kept for compatibility reason. Signed-off-by: Knut Ahlers <knut@ahlers.me> Signed-off-by: Knut Ahlers <knut@ahlers.me> closes #14 closes #15
500 lines
13 KiB
Go
500 lines
13 KiB
Go
// Copyright 2014 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/dsa"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/ed25519"
|
|
"golang.org/x/crypto/ssh/testdata"
|
|
)
|
|
|
|
func rawKey(pub PublicKey) interface{} {
|
|
switch k := pub.(type) {
|
|
case *rsaPublicKey:
|
|
return (*rsa.PublicKey)(k)
|
|
case *dsaPublicKey:
|
|
return (*dsa.PublicKey)(k)
|
|
case *ecdsaPublicKey:
|
|
return (*ecdsa.PublicKey)(k)
|
|
case ed25519PublicKey:
|
|
return (ed25519.PublicKey)(k)
|
|
case *Certificate:
|
|
return k
|
|
}
|
|
panic("unknown key type")
|
|
}
|
|
|
|
func TestKeyMarshalParse(t *testing.T) {
|
|
for _, priv := range testSigners {
|
|
pub := priv.PublicKey()
|
|
roundtrip, err := ParsePublicKey(pub.Marshal())
|
|
if err != nil {
|
|
t.Errorf("ParsePublicKey(%T): %v", pub, err)
|
|
}
|
|
|
|
k1 := rawKey(pub)
|
|
k2 := rawKey(roundtrip)
|
|
|
|
if !reflect.DeepEqual(k1, k2) {
|
|
t.Errorf("got %#v in roundtrip, want %#v", k2, k1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnsupportedCurves(t *testing.T) {
|
|
raw, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey: %v", err)
|
|
}
|
|
|
|
if _, err = NewSignerFromKey(raw); err == nil || !strings.Contains(err.Error(), "only P-256") {
|
|
t.Fatalf("NewPrivateKey should not succeed with P-224, got: %v", err)
|
|
}
|
|
|
|
if _, err = NewPublicKey(&raw.PublicKey); err == nil || !strings.Contains(err.Error(), "only P-256") {
|
|
t.Fatalf("NewPublicKey should not succeed with P-224, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewPublicKey(t *testing.T) {
|
|
for _, k := range testSigners {
|
|
raw := rawKey(k.PublicKey())
|
|
// Skip certificates, as NewPublicKey does not support them.
|
|
if _, ok := raw.(*Certificate); ok {
|
|
continue
|
|
}
|
|
pub, err := NewPublicKey(raw)
|
|
if err != nil {
|
|
t.Errorf("NewPublicKey(%#v): %v", raw, err)
|
|
}
|
|
if !reflect.DeepEqual(k.PublicKey(), pub) {
|
|
t.Errorf("NewPublicKey(%#v) = %#v, want %#v", raw, pub, k.PublicKey())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKeySignVerify(t *testing.T) {
|
|
for _, priv := range testSigners {
|
|
pub := priv.PublicKey()
|
|
|
|
data := []byte("sign me")
|
|
sig, err := priv.Sign(rand.Reader, data)
|
|
if err != nil {
|
|
t.Fatalf("Sign(%T): %v", priv, err)
|
|
}
|
|
|
|
if err := pub.Verify(data, sig); err != nil {
|
|
t.Errorf("publicKey.Verify(%T): %v", priv, err)
|
|
}
|
|
sig.Blob[5]++
|
|
if err := pub.Verify(data, sig); err == nil {
|
|
t.Errorf("publicKey.Verify on broken sig did not fail")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseRSAPrivateKey(t *testing.T) {
|
|
key := testPrivateKeys["rsa"]
|
|
|
|
rsa, ok := key.(*rsa.PrivateKey)
|
|
if !ok {
|
|
t.Fatalf("got %T, want *rsa.PrivateKey", rsa)
|
|
}
|
|
|
|
if err := rsa.Validate(); err != nil {
|
|
t.Errorf("Validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseECPrivateKey(t *testing.T) {
|
|
key := testPrivateKeys["ecdsa"]
|
|
|
|
ecKey, ok := key.(*ecdsa.PrivateKey)
|
|
if !ok {
|
|
t.Fatalf("got %T, want *ecdsa.PrivateKey", ecKey)
|
|
}
|
|
|
|
if !validateECPublicKey(ecKey.Curve, ecKey.X, ecKey.Y) {
|
|
t.Fatalf("public key does not validate.")
|
|
}
|
|
}
|
|
|
|
// See Issue https://github.com/golang/go/issues/6650.
|
|
func TestParseEncryptedPrivateKeysFails(t *testing.T) {
|
|
const wantSubstring = "encrypted"
|
|
for i, tt := range testdata.PEMEncryptedKeys {
|
|
_, err := ParsePrivateKey(tt.PEMBytes)
|
|
if err == nil {
|
|
t.Errorf("#%d key %s: ParsePrivateKey successfully parsed, expected an error", i, tt.Name)
|
|
continue
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), wantSubstring) {
|
|
t.Errorf("#%d key %s: got error %q, want substring %q", i, tt.Name, err, wantSubstring)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse encrypted private keys with passphrase
|
|
func TestParseEncryptedPrivateKeysWithPassphrase(t *testing.T) {
|
|
data := []byte("sign me")
|
|
for _, tt := range testdata.PEMEncryptedKeys {
|
|
s, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey))
|
|
if err != nil {
|
|
t.Fatalf("ParsePrivateKeyWithPassphrase returned error: %s", err)
|
|
continue
|
|
}
|
|
sig, err := s.Sign(rand.Reader, data)
|
|
if err != nil {
|
|
t.Fatalf("dsa.Sign: %v", err)
|
|
}
|
|
if err := s.PublicKey().Verify(data, sig); err != nil {
|
|
t.Errorf("Verify failed: %v", err)
|
|
}
|
|
}
|
|
|
|
tt := testdata.PEMEncryptedKeys[0]
|
|
_, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte("incorrect"))
|
|
if err != x509.IncorrectPasswordError {
|
|
t.Fatalf("got %v want IncorrectPasswordError", err)
|
|
}
|
|
}
|
|
|
|
func TestParseDSA(t *testing.T) {
|
|
// We actually exercise the ParsePrivateKey codepath here, as opposed to
|
|
// using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go
|
|
// uses.
|
|
s, err := ParsePrivateKey(testdata.PEMBytes["dsa"])
|
|
if err != nil {
|
|
t.Fatalf("ParsePrivateKey returned error: %s", err)
|
|
}
|
|
|
|
data := []byte("sign me")
|
|
sig, err := s.Sign(rand.Reader, data)
|
|
if err != nil {
|
|
t.Fatalf("dsa.Sign: %v", err)
|
|
}
|
|
|
|
if err := s.PublicKey().Verify(data, sig); err != nil {
|
|
t.Errorf("Verify failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Tests for authorized_keys parsing.
|
|
|
|
// getTestKey returns a public key, and its base64 encoding.
|
|
func getTestKey() (PublicKey, string) {
|
|
k := testPublicKeys["rsa"]
|
|
|
|
b := &bytes.Buffer{}
|
|
e := base64.NewEncoder(base64.StdEncoding, b)
|
|
e.Write(k.Marshal())
|
|
e.Close()
|
|
|
|
return k, b.String()
|
|
}
|
|
|
|
func TestMarshalParsePublicKey(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
line := fmt.Sprintf("%s %s user@host", pub.Type(), pubSerialized)
|
|
|
|
authKeys := MarshalAuthorizedKey(pub)
|
|
actualFields := strings.Fields(string(authKeys))
|
|
if len(actualFields) == 0 {
|
|
t.Fatalf("failed authKeys: %v", authKeys)
|
|
}
|
|
|
|
// drop the comment
|
|
expectedFields := strings.Fields(line)[0:2]
|
|
|
|
if !reflect.DeepEqual(actualFields, expectedFields) {
|
|
t.Errorf("got %v, expected %v", actualFields, expectedFields)
|
|
}
|
|
|
|
actPub, _, _, _, err := ParseAuthorizedKey([]byte(line))
|
|
if err != nil {
|
|
t.Fatalf("cannot parse %v: %v", line, err)
|
|
}
|
|
if !reflect.DeepEqual(actPub, pub) {
|
|
t.Errorf("got %v, expected %v", actPub, pub)
|
|
}
|
|
}
|
|
|
|
type authResult struct {
|
|
pubKey PublicKey
|
|
options []string
|
|
comments string
|
|
rest string
|
|
ok bool
|
|
}
|
|
|
|
func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []authResult) {
|
|
rest := authKeys
|
|
var values []authResult
|
|
for len(rest) > 0 {
|
|
var r authResult
|
|
var err error
|
|
r.pubKey, r.comments, r.options, rest, err = ParseAuthorizedKey(rest)
|
|
r.ok = (err == nil)
|
|
t.Log(err)
|
|
r.rest = string(rest)
|
|
values = append(values, r)
|
|
}
|
|
|
|
if !reflect.DeepEqual(values, expected) {
|
|
t.Errorf("got %#v, expected %#v", values, expected)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizedKeyBasic(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
line := "ssh-rsa " + pubSerialized + " user@host"
|
|
testAuthorizedKeys(t, []byte(line),
|
|
[]authResult{
|
|
{pub, nil, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
func TestAuth(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
authWithOptions := []string{
|
|
`# comments to ignore before any keys...`,
|
|
``,
|
|
`env="HOME=/home/root",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`,
|
|
`# comments to ignore, along with a blank line`,
|
|
``,
|
|
`env="HOME=/home/root2" ssh-rsa ` + pubSerialized + ` user2@host2`,
|
|
``,
|
|
`# more comments, plus a invalid entry`,
|
|
`ssh-rsa data-that-will-not-parse user@host3`,
|
|
}
|
|
for _, eol := range []string{"\n", "\r\n"} {
|
|
authOptions := strings.Join(authWithOptions, eol)
|
|
rest2 := strings.Join(authWithOptions[3:], eol)
|
|
rest3 := strings.Join(authWithOptions[6:], eol)
|
|
testAuthorizedKeys(t, []byte(authOptions), []authResult{
|
|
{pub, []string{`env="HOME=/home/root"`, "no-port-forwarding"}, "user@host", rest2, true},
|
|
{pub, []string{`env="HOME=/home/root2"`}, "user2@host2", rest3, true},
|
|
{nil, nil, "", "", false},
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthWithQuotedSpaceInEnv(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
authWithQuotedSpaceInEnv := []byte(`env="HOME=/home/root dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`)
|
|
testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []authResult{
|
|
{pub, []string{`env="HOME=/home/root dir"`, "no-port-forwarding"}, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
func TestAuthWithQuotedCommaInEnv(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
authWithQuotedCommaInEnv := []byte(`env="HOME=/home/root,dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`)
|
|
testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []authResult{
|
|
{pub, []string{`env="HOME=/home/root,dir"`, "no-port-forwarding"}, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
func TestAuthWithQuotedQuoteInEnv(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
authWithQuotedQuoteInEnv := []byte(`env="HOME=/home/\"root dir",no-port-forwarding` + "\t" + `ssh-rsa` + "\t" + pubSerialized + ` user@host`)
|
|
authWithDoubleQuotedQuote := []byte(`no-port-forwarding,env="HOME=/home/ \"root dir\"" ssh-rsa ` + pubSerialized + "\t" + `user@host`)
|
|
testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []authResult{
|
|
{pub, []string{`env="HOME=/home/\"root dir"`, "no-port-forwarding"}, "user@host", "", true},
|
|
})
|
|
|
|
testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []authResult{
|
|
{pub, []string{"no-port-forwarding", `env="HOME=/home/ \"root dir\""`}, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
func TestAuthWithInvalidSpace(t *testing.T) {
|
|
_, pubSerialized := getTestKey()
|
|
authWithInvalidSpace := []byte(`env="HOME=/home/root dir", no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host
|
|
#more to follow but still no valid keys`)
|
|
testAuthorizedKeys(t, []byte(authWithInvalidSpace), []authResult{
|
|
{nil, nil, "", "", false},
|
|
})
|
|
}
|
|
|
|
func TestAuthWithMissingQuote(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
authWithMissingQuote := []byte(`env="HOME=/home/root,no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host
|
|
env="HOME=/home/root",shared-control ssh-rsa ` + pubSerialized + ` user@host`)
|
|
|
|
testAuthorizedKeys(t, []byte(authWithMissingQuote), []authResult{
|
|
{pub, []string{`env="HOME=/home/root"`, `shared-control`}, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
func TestInvalidEntry(t *testing.T) {
|
|
authInvalid := []byte(`ssh-rsa`)
|
|
_, _, _, _, err := ParseAuthorizedKey(authInvalid)
|
|
if err == nil {
|
|
t.Errorf("got valid entry for %q", authInvalid)
|
|
}
|
|
}
|
|
|
|
var knownHostsParseTests = []struct {
|
|
input string
|
|
err string
|
|
|
|
marker string
|
|
comment string
|
|
hosts []string
|
|
rest string
|
|
}{
|
|
{
|
|
"",
|
|
"EOF",
|
|
|
|
"", "", nil, "",
|
|
},
|
|
{
|
|
"# Just a comment",
|
|
"EOF",
|
|
|
|
"", "", nil, "",
|
|
},
|
|
{
|
|
" \t ",
|
|
"EOF",
|
|
|
|
"", "", nil, "",
|
|
},
|
|
{
|
|
"localhost ssh-rsa {RSAPUB}",
|
|
"",
|
|
|
|
"", "", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}",
|
|
"",
|
|
|
|
"", "", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}\tcomment comment",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}\tcomment comment\n",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}\tcomment comment\r\n",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}\tcomment comment\r\nnext line",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost"}, "next line",
|
|
},
|
|
{
|
|
"localhost,[host2:123]\tssh-rsa {RSAPUB}\tcomment comment",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost", "[host2:123]"}, "",
|
|
},
|
|
{
|
|
"@marker \tlocalhost,[host2:123]\tssh-rsa {RSAPUB}",
|
|
"",
|
|
|
|
"marker", "", []string{"localhost", "[host2:123]"}, "",
|
|
},
|
|
{
|
|
"@marker \tlocalhost,[host2:123]\tssh-rsa aabbccdd",
|
|
"short read",
|
|
|
|
"", "", nil, "",
|
|
},
|
|
}
|
|
|
|
func TestKnownHostsParsing(t *testing.T) {
|
|
rsaPub, rsaPubSerialized := getTestKey()
|
|
|
|
for i, test := range knownHostsParseTests {
|
|
var expectedKey PublicKey
|
|
const rsaKeyToken = "{RSAPUB}"
|
|
|
|
input := test.input
|
|
if strings.Contains(input, rsaKeyToken) {
|
|
expectedKey = rsaPub
|
|
input = strings.Replace(test.input, rsaKeyToken, rsaPubSerialized, -1)
|
|
}
|
|
|
|
marker, hosts, pubKey, comment, rest, err := ParseKnownHosts([]byte(input))
|
|
if err != nil {
|
|
if len(test.err) == 0 {
|
|
t.Errorf("#%d: unexpectedly failed with %q", i, err)
|
|
} else if !strings.Contains(err.Error(), test.err) {
|
|
t.Errorf("#%d: expected error containing %q, but got %q", i, test.err, err)
|
|
}
|
|
continue
|
|
} else if len(test.err) != 0 {
|
|
t.Errorf("#%d: succeeded but expected error including %q", i, test.err)
|
|
continue
|
|
}
|
|
|
|
if !reflect.DeepEqual(expectedKey, pubKey) {
|
|
t.Errorf("#%d: expected key %#v, but got %#v", i, expectedKey, pubKey)
|
|
}
|
|
|
|
if marker != test.marker {
|
|
t.Errorf("#%d: expected marker %q, but got %q", i, test.marker, marker)
|
|
}
|
|
|
|
if comment != test.comment {
|
|
t.Errorf("#%d: expected comment %q, but got %q", i, test.comment, comment)
|
|
}
|
|
|
|
if !reflect.DeepEqual(test.hosts, hosts) {
|
|
t.Errorf("#%d: expected hosts %#v, but got %#v", i, test.hosts, hosts)
|
|
}
|
|
|
|
if rest := string(rest); rest != test.rest {
|
|
t.Errorf("#%d: expected remaining input to be %q, but got %q", i, test.rest, rest)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFingerprintLegacyMD5(t *testing.T) {
|
|
pub, _ := getTestKey()
|
|
fingerprint := FingerprintLegacyMD5(pub)
|
|
want := "fb:61:6d:1a:e3:f0:95:45:3c:a0:79:be:4a:93:63:66" // ssh-keygen -lf -E md5 rsa
|
|
if fingerprint != want {
|
|
t.Errorf("got fingerprint %q want %q", fingerprint, want)
|
|
}
|
|
}
|
|
|
|
func TestFingerprintSHA256(t *testing.T) {
|
|
pub, _ := getTestKey()
|
|
fingerprint := FingerprintSHA256(pub)
|
|
want := "SHA256:Anr3LjZK8YVpjrxu79myrW9Hrb/wpcMNpVvTq/RcBm8" // ssh-keygen -lf rsa
|
|
if fingerprint != want {
|
|
t.Errorf("got fingerprint %q want %q", fingerprint, want)
|
|
}
|
|
}
|