| // Copyright 2012 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 agent |
| |
| import ( |
| "bytes" |
| "crypto/rand" |
| "errors" |
| "io" |
| "net" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "strconv" |
| "strings" |
| "testing" |
| "time" |
| |
| "golang.org/x/crypto/ssh" |
| ) |
| |
| // startOpenSSHAgent executes ssh-agent, and returns an Agent interface to it. |
| func startOpenSSHAgent(t *testing.T) (client ExtendedAgent, socket string, cleanup func()) { |
| if testing.Short() { |
| // ssh-agent is not always available, and the key |
| // types supported vary by platform. |
| t.Skip("skipping test due to -short") |
| } |
| if runtime.GOOS == "windows" { |
| t.Skip("skipping on windows, we don't support connecting to the ssh-agent via a named pipe") |
| } |
| |
| bin, err := exec.LookPath("ssh-agent") |
| if err != nil { |
| t.Skip("could not find ssh-agent") |
| } |
| |
| cmd := exec.Command(bin, "-s") |
| cmd.Env = []string{} // Do not let the user's environment influence ssh-agent behavior. |
| cmd.Stderr = new(bytes.Buffer) |
| out, err := cmd.Output() |
| if err != nil { |
| t.Fatalf("%s failed: %v\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr) |
| } |
| |
| // Output looks like: |
| // |
| // SSH_AUTH_SOCK=/tmp/ssh-P65gpcqArqvH/agent.15541; export SSH_AUTH_SOCK; |
| // SSH_AGENT_PID=15542; export SSH_AGENT_PID; |
| // echo Agent pid 15542; |
| |
| fields := bytes.Split(out, []byte(";")) |
| line := bytes.SplitN(fields[0], []byte("="), 2) |
| line[0] = bytes.TrimLeft(line[0], "\n") |
| if string(line[0]) != "SSH_AUTH_SOCK" { |
| t.Fatalf("could not find key SSH_AUTH_SOCK in %q", fields[0]) |
| } |
| socket = string(line[1]) |
| |
| line = bytes.SplitN(fields[2], []byte("="), 2) |
| line[0] = bytes.TrimLeft(line[0], "\n") |
| if string(line[0]) != "SSH_AGENT_PID" { |
| t.Fatalf("could not find key SSH_AGENT_PID in %q", fields[2]) |
| } |
| pidStr := line[1] |
| pid, err := strconv.Atoi(string(pidStr)) |
| if err != nil { |
| t.Fatalf("Atoi(%q): %v", pidStr, err) |
| } |
| |
| conn, err := net.Dial("unix", string(socket)) |
| if err != nil { |
| t.Fatalf("net.Dial: %v", err) |
| } |
| |
| ac := NewClient(conn) |
| return ac, socket, func() { |
| proc, _ := os.FindProcess(pid) |
| if proc != nil { |
| proc.Kill() |
| } |
| conn.Close() |
| os.RemoveAll(filepath.Dir(socket)) |
| } |
| } |
| |
| func startAgent(t *testing.T, agent Agent) (client ExtendedAgent, cleanup func()) { |
| c1, c2, err := netPipe() |
| if err != nil { |
| t.Fatalf("netPipe: %v", err) |
| } |
| go ServeAgent(agent, c2) |
| |
| return NewClient(c1), func() { |
| c1.Close() |
| c2.Close() |
| } |
| } |
| |
| // startKeyringAgent uses Keyring to simulate a ssh-agent Server and returns a client. |
| func startKeyringAgent(t *testing.T) (client ExtendedAgent, cleanup func()) { |
| return startAgent(t, NewKeyring()) |
| } |
| |
| func testOpenSSHAgent(t *testing.T, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) { |
| agent, _, cleanup := startOpenSSHAgent(t) |
| defer cleanup() |
| |
| testAgentInterface(t, agent, key, cert, lifetimeSecs) |
| } |
| |
| func testKeyringAgent(t *testing.T, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) { |
| agent, cleanup := startKeyringAgent(t) |
| defer cleanup() |
| |
| testAgentInterface(t, agent, key, cert, lifetimeSecs) |
| } |
| |
| func testAgentInterface(t *testing.T, agent ExtendedAgent, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) { |
| signer, err := ssh.NewSignerFromKey(key) |
| if err != nil { |
| t.Fatalf("NewSignerFromKey(%T): %v", key, err) |
| } |
| // The agent should start up empty. |
| if keys, err := agent.List(); err != nil { |
| t.Fatalf("RequestIdentities: %v", err) |
| } else if len(keys) > 0 { |
| t.Fatalf("got %d keys, want 0: %v", len(keys), keys) |
| } |
| |
| // Attempt to insert the key, with certificate if specified. |
| var pubKey ssh.PublicKey |
| if cert != nil { |
| err = agent.Add(AddedKey{ |
| PrivateKey: key, |
| Certificate: cert, |
| Comment: "comment", |
| LifetimeSecs: lifetimeSecs, |
| }) |
| pubKey = cert |
| } else { |
| err = agent.Add(AddedKey{PrivateKey: key, Comment: "comment", LifetimeSecs: lifetimeSecs}) |
| pubKey = signer.PublicKey() |
| } |
| if err != nil { |
| t.Fatalf("insert(%T): %v", key, err) |
| } |
| |
| // Did the key get inserted successfully? |
| if keys, err := agent.List(); err != nil { |
| t.Fatalf("List: %v", err) |
| } else if len(keys) != 1 { |
| t.Fatalf("got %v, want 1 key", keys) |
| } else if keys[0].Comment != "comment" { |
| t.Fatalf("key comment: got %v, want %v", keys[0].Comment, "comment") |
| } else if !bytes.Equal(keys[0].Blob, pubKey.Marshal()) { |
| t.Fatalf("key mismatch") |
| } |
| |
| // Can the agent make a valid signature? |
| data := []byte("hello") |
| sig, err := agent.Sign(pubKey, data) |
| if err != nil { |
| t.Logf("sign failed with key type %q", pubKey.Type()) |
| // In integration tests ssh-dss and ssh-rsa (SHA1 signatures) may be |
| // disabled for security reasons, we check SHA-2 variants later. |
| if pubKey.Type() != ssh.KeyAlgoDSA && pubKey.Type() != ssh.KeyAlgoRSA && pubKey.Type() != ssh.CertAlgoRSAv01 { |
| t.Fatalf("Sign(%s): %v", pubKey.Type(), err) |
| } |
| } else { |
| if err := pubKey.Verify(data, sig); err != nil { |
| t.Logf("verify failed with key type %q", pubKey.Type()) |
| if pubKey.Type() != ssh.KeyAlgoRSA { |
| t.Fatalf("Verify(%s): %v", pubKey.Type(), err) |
| } |
| } |
| } |
| |
| // For tests on RSA keys, try signing with SHA-256 and SHA-512 flags |
| if pubKey.Type() == ssh.KeyAlgoRSA { |
| sshFlagTest := func(flag SignatureFlags, expectedSigFormat string) { |
| sig, err = agent.SignWithFlags(pubKey, data, flag) |
| if err != nil { |
| t.Fatalf("SignWithFlags(%s): %v", pubKey.Type(), err) |
| } |
| if sig.Format != expectedSigFormat { |
| t.Fatalf("Signature format didn't match expected value: %s != %s", sig.Format, expectedSigFormat) |
| } |
| if err := pubKey.Verify(data, sig); err != nil { |
| t.Fatalf("Verify(%s): %v", pubKey.Type(), err) |
| } |
| } |
| sshFlagTest(SignatureFlagRsaSha256, ssh.KeyAlgoRSASHA256) |
| sshFlagTest(SignatureFlagRsaSha512, ssh.KeyAlgoRSASHA512) |
| } |
| |
| // If the key has a lifetime, is it removed when it should be? |
| if lifetimeSecs > 0 { |
| time.Sleep(time.Second*time.Duration(lifetimeSecs) + 100*time.Millisecond) |
| keys, err := agent.List() |
| if err != nil { |
| t.Fatalf("List: %v", err) |
| } |
| if len(keys) > 0 { |
| t.Fatalf("key not expired") |
| } |
| } |
| |
| } |
| |
| func TestMalformedRequests(t *testing.T) { |
| keyringAgent := NewKeyring() |
| |
| testCase := func(t *testing.T, requestBytes []byte, wantServerErr bool) { |
| c, s := net.Pipe() |
| defer c.Close() |
| defer s.Close() |
| go func() { |
| _, err := c.Write(requestBytes) |
| if err != nil { |
| t.Errorf("Unexpected error writing raw bytes on connection: %v", err) |
| } |
| c.Close() |
| }() |
| err := ServeAgent(keyringAgent, s) |
| if err == nil { |
| t.Error("ServeAgent should have returned an error to malformed input") |
| } else { |
| if (err != io.EOF) != wantServerErr { |
| t.Errorf("ServeAgent returned expected error: %v", err) |
| } |
| } |
| } |
| |
| var testCases = []struct { |
| name string |
| requestBytes []byte |
| wantServerErr bool |
| }{ |
| {"Empty request", []byte{}, false}, |
| {"Short header", []byte{0x00}, true}, |
| {"Empty body", []byte{0x00, 0x00, 0x00, 0x00}, true}, |
| {"Short body", []byte{0x00, 0x00, 0x00, 0x01}, false}, |
| } |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { testCase(t, tc.requestBytes, tc.wantServerErr) }) |
| } |
| } |
| |
| func TestAgent(t *testing.T) { |
| for _, keyType := range []string{"rsa", "dsa", "ecdsa", "ed25519"} { |
| testOpenSSHAgent(t, testPrivateKeys[keyType], nil, 0) |
| testKeyringAgent(t, testPrivateKeys[keyType], nil, 0) |
| } |
| } |
| |
| func TestCert(t *testing.T) { |
| cert := &ssh.Certificate{ |
| Key: testPublicKeys["rsa"], |
| ValidBefore: ssh.CertTimeInfinity, |
| CertType: ssh.UserCert, |
| } |
| cert.SignCert(rand.Reader, testSigners["ecdsa"]) |
| |
| testOpenSSHAgent(t, testPrivateKeys["rsa"], cert, 0) |
| testKeyringAgent(t, testPrivateKeys["rsa"], cert, 0) |
| } |
| |
| // netListener creates a localhost network listener. |
| func netListener() (net.Listener, error) { |
| listener, err := net.Listen("tcp", "127.0.0.1:0") |
| if err != nil { |
| listener, err = net.Listen("tcp", "[::1]:0") |
| if err != nil { |
| return nil, err |
| } |
| } |
| return listener, nil |
| } |
| |
| // netPipe is analogous to net.Pipe, but it uses a real net.Conn, and |
| // therefore is buffered (net.Pipe deadlocks if both sides start with |
| // a write.) |
| func netPipe() (net.Conn, net.Conn, error) { |
| listener, err := netListener() |
| if err != nil { |
| return nil, nil, err |
| } |
| defer listener.Close() |
| c1, err := net.Dial("tcp", listener.Addr().String()) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| c2, err := listener.Accept() |
| if err != nil { |
| c1.Close() |
| return nil, nil, err |
| } |
| |
| return c1, c2, nil |
| } |
| |
| func TestServerResponseTooLarge(t *testing.T) { |
| a, b, err := netPipe() |
| if err != nil { |
| t.Fatalf("netPipe: %v", err) |
| } |
| done := make(chan struct{}) |
| defer func() { <-done }() |
| |
| defer a.Close() |
| defer b.Close() |
| |
| var response identitiesAnswerAgentMsg |
| response.NumKeys = 1 |
| response.Keys = make([]byte, maxAgentResponseBytes+1) |
| |
| agent := NewClient(a) |
| go func() { |
| defer close(done) |
| n, err := b.Write(ssh.Marshal(response)) |
| if n < 4 { |
| if runtime.GOOS == "plan9" { |
| if e1, ok := err.(*net.OpError); ok { |
| if e2, ok := e1.Err.(*os.PathError); ok { |
| switch e2.Err.Error() { |
| case "Hangup", "i/o on hungup channel": |
| // syscall.Pwrite returns -1 in this case even when some data did get written. |
| return |
| } |
| } |
| } |
| } |
| t.Errorf("At least 4 bytes (the response size) should have been successfully written: %d < 4: %v", n, err) |
| } |
| }() |
| _, err = agent.List() |
| if err == nil { |
| t.Fatal("Did not get error result") |
| } |
| if err.Error() != "agent: client error: response too large" { |
| t.Fatal("Did not get expected error result") |
| } |
| } |
| |
| func TestAuth(t *testing.T) { |
| agent, _, cleanup := startOpenSSHAgent(t) |
| defer cleanup() |
| |
| a, b, err := netPipe() |
| if err != nil { |
| t.Fatalf("netPipe: %v", err) |
| } |
| |
| defer a.Close() |
| defer b.Close() |
| |
| if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"], Comment: "comment"}); err != nil { |
| t.Errorf("Add: %v", err) |
| } |
| |
| serverConf := ssh.ServerConfig{} |
| serverConf.AddHostKey(testSigners["rsa"]) |
| serverConf.PublicKeyCallback = func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { |
| return nil, nil |
| } |
| |
| return nil, errors.New("pubkey rejected") |
| } |
| |
| go func() { |
| conn, _, _, err := ssh.NewServerConn(a, &serverConf) |
| if err != nil { |
| t.Errorf("NewServerConn error: %v", err) |
| return |
| } |
| conn.Close() |
| }() |
| |
| conf := ssh.ClientConfig{ |
| HostKeyCallback: ssh.InsecureIgnoreHostKey(), |
| } |
| conf.Auth = append(conf.Auth, ssh.PublicKeysCallback(agent.Signers)) |
| conn, _, _, err := ssh.NewClientConn(b, "", &conf) |
| if err != nil { |
| t.Fatalf("NewClientConn: %v", err) |
| } |
| conn.Close() |
| } |
| |
| func TestLockOpenSSHAgent(t *testing.T) { |
| agent, _, cleanup := startOpenSSHAgent(t) |
| defer cleanup() |
| testLockAgent(agent, t) |
| } |
| |
| func TestLockKeyringAgent(t *testing.T) { |
| agent, cleanup := startKeyringAgent(t) |
| defer cleanup() |
| testLockAgent(agent, t) |
| } |
| |
| func testLockAgent(agent Agent, t *testing.T) { |
| if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"], Comment: "comment 1"}); err != nil { |
| t.Errorf("Add: %v", err) |
| } |
| if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["dsa"], Comment: "comment dsa"}); err != nil { |
| t.Errorf("Add: %v", err) |
| } |
| if keys, err := agent.List(); err != nil { |
| t.Errorf("List: %v", err) |
| } else if len(keys) != 2 { |
| t.Errorf("Want 2 keys, got %v", keys) |
| } |
| |
| passphrase := []byte("secret") |
| if err := agent.Lock(passphrase); err != nil { |
| t.Errorf("Lock: %v", err) |
| } |
| |
| if keys, err := agent.List(); err != nil { |
| t.Errorf("List: %v", err) |
| } else if len(keys) != 0 { |
| t.Errorf("Want 0 keys, got %v", keys) |
| } |
| |
| signer, _ := ssh.NewSignerFromKey(testPrivateKeys["rsa"]) |
| if _, err := agent.Sign(signer.PublicKey(), []byte("hello")); err == nil { |
| t.Fatalf("Sign did not fail") |
| } |
| |
| if err := agent.Remove(signer.PublicKey()); err == nil { |
| t.Fatalf("Remove did not fail") |
| } |
| |
| if err := agent.RemoveAll(); err == nil { |
| t.Fatalf("RemoveAll did not fail") |
| } |
| |
| if err := agent.Unlock(nil); err == nil { |
| t.Errorf("Unlock with wrong passphrase succeeded") |
| } |
| if err := agent.Unlock(passphrase); err != nil { |
| t.Errorf("Unlock: %v", err) |
| } |
| |
| if err := agent.Remove(signer.PublicKey()); err != nil { |
| t.Fatalf("Remove: %v", err) |
| } |
| |
| if keys, err := agent.List(); err != nil { |
| t.Errorf("List: %v", err) |
| } else if len(keys) != 1 { |
| t.Errorf("Want 1 keys, got %v", keys) |
| } |
| } |
| |
| func testOpenSSHAgentLifetime(t *testing.T) { |
| agent, _, cleanup := startOpenSSHAgent(t) |
| defer cleanup() |
| testAgentLifetime(t, agent) |
| } |
| |
| func testKeyringAgentLifetime(t *testing.T) { |
| agent, cleanup := startKeyringAgent(t) |
| defer cleanup() |
| testAgentLifetime(t, agent) |
| } |
| |
| func testAgentLifetime(t *testing.T, agent Agent) { |
| for _, keyType := range []string{"rsa", "dsa", "ecdsa"} { |
| // Add private keys to the agent. |
| err := agent.Add(AddedKey{ |
| PrivateKey: testPrivateKeys[keyType], |
| Comment: "comment", |
| LifetimeSecs: 1, |
| }) |
| if err != nil { |
| t.Fatalf("add: %v", err) |
| } |
| // Add certs to the agent. |
| cert := &ssh.Certificate{ |
| Key: testPublicKeys[keyType], |
| ValidBefore: ssh.CertTimeInfinity, |
| CertType: ssh.UserCert, |
| } |
| cert.SignCert(rand.Reader, testSigners[keyType]) |
| err = agent.Add(AddedKey{ |
| PrivateKey: testPrivateKeys[keyType], |
| Certificate: cert, |
| Comment: "comment", |
| LifetimeSecs: 1, |
| }) |
| if err != nil { |
| t.Fatalf("add: %v", err) |
| } |
| } |
| time.Sleep(1100 * time.Millisecond) |
| if keys, err := agent.List(); err != nil { |
| t.Errorf("List: %v", err) |
| } else if len(keys) != 0 { |
| t.Errorf("Want 0 keys, got %v", len(keys)) |
| } |
| } |
| |
| type keyringExtended struct { |
| *keyring |
| } |
| |
| func (r *keyringExtended) Extension(extensionType string, contents []byte) ([]byte, error) { |
| if extensionType != "my-extension@example.com" { |
| return []byte{agentExtensionFailure}, nil |
| } |
| return append([]byte{agentSuccess}, contents...), nil |
| } |
| |
| func TestAgentExtensions(t *testing.T) { |
| agent, _, cleanup := startOpenSSHAgent(t) |
| defer cleanup() |
| _, err := agent.Extension("my-extension@example.com", []byte{0x00, 0x01, 0x02}) |
| if err == nil { |
| t.Fatal("should have gotten agent extension failure") |
| } |
| |
| agent, cleanup = startAgent(t, &keyringExtended{}) |
| defer cleanup() |
| result, err := agent.Extension("my-extension@example.com", []byte{0x00, 0x01, 0x02}) |
| if err != nil { |
| t.Fatalf("agent extension failure: %v", err) |
| } |
| if len(result) != 4 || !bytes.Equal(result, []byte{agentSuccess, 0x00, 0x01, 0x02}) { |
| t.Fatalf("agent extension result invalid: %v", result) |
| } |
| |
| _, err = agent.Extension("bad-extension@example.com", []byte{0x00, 0x01, 0x02}) |
| if err == nil { |
| t.Fatal("should have gotten agent extension failure") |
| } |
| } |