ForwardToAgent and ForwardToRemote only read the main stream of the auth-agent@openssh.com channels they accept. If a peer sends data on the channel's extended (stderr) stream the bytes accumulate in the client-side extPending buffer and the receive window is never replenished, because the window is only adjusted as a side effect of ReadExtended. That can pin up to channelWindowSize (2 MiB) of memory per channel and silently stalls any stderr traffic once the window is exhausted. The auth-agent protocol does not use stderr, so a well-behaved peer never sends anything on it. To stay tolerant of misbehaving peers without leaving the channel half-stuck, drain the stderr stream into io.Discard, mirroring the existing DiscardRequests pattern. The goroutine exits when the channel is closed because Stderr().Read returns io.EOF. Add a regression test that opens an agent-forwarding channel and writes more than the default window on the stderr stream from the server side. Without the fix the write blocks once the remote window is exhausted; with the fix the bytes are drained and the agent stream remains usable. Change-Id: Iadf8ea6ca726c058421bbc39f92e0100579fda17 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/783720 Reviewed-by: Filippo Valsorda <filippo@golang.org> Reviewed-by: Carlos Amedee <carlos@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
397 lines
9.3 KiB
Go
397 lines
9.3 KiB
Go
// 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 (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
pseudorand "math/rand"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func TestServer(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
client := NewClient(c1)
|
|
|
|
go ServeAgent(NewKeyring(), c2)
|
|
|
|
testAgentInterface(t, client, testPrivateKeys["rsa"], nil, 0)
|
|
}
|
|
|
|
func TestLockServer(t *testing.T) {
|
|
testLockAgent(NewKeyring(), t)
|
|
}
|
|
|
|
func TestSetupForwardAgent(t *testing.T) {
|
|
a, b, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
_, socket, cleanup := startOpenSSHAgent(t)
|
|
defer cleanup()
|
|
|
|
serverConf := ssh.ServerConfig{
|
|
NoClientAuth: true,
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
incoming := make(chan *ssh.ServerConn, 1)
|
|
go func() {
|
|
conn, _, _, err := ssh.NewServerConn(a, &serverConf)
|
|
incoming <- conn
|
|
if err != nil {
|
|
t.Errorf("NewServerConn error: %v", err)
|
|
return
|
|
}
|
|
}()
|
|
|
|
conf := ssh.ClientConfig{
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
}
|
|
conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf)
|
|
if err != nil {
|
|
t.Fatalf("NewClientConn: %v", err)
|
|
}
|
|
client := ssh.NewClient(conn, chans, reqs)
|
|
|
|
if err := ForwardToRemote(client, socket); err != nil {
|
|
t.Fatalf("SetupForwardAgent: %v", err)
|
|
}
|
|
server := <-incoming
|
|
if server == nil {
|
|
t.Fatal("Unable to get server")
|
|
}
|
|
ch, reqs, err := server.OpenChannel(channelType, nil)
|
|
if err != nil {
|
|
t.Fatalf("OpenChannel(%q): %v", channelType, err)
|
|
}
|
|
go ssh.DiscardRequests(reqs)
|
|
|
|
agentClient := NewClient(ch)
|
|
testAgentInterface(t, agentClient, testPrivateKeys["rsa"], nil, 0)
|
|
conn.Close()
|
|
}
|
|
|
|
func TestForwardAgentDiscardsStderr(t *testing.T) {
|
|
a, b, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
serverConf := ssh.ServerConfig{
|
|
NoClientAuth: true,
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
incoming := make(chan *ssh.ServerConn, 1)
|
|
go func() {
|
|
conn, _, _, err := ssh.NewServerConn(a, &serverConf)
|
|
incoming <- conn
|
|
if err != nil {
|
|
t.Errorf("NewServerConn error: %v", err)
|
|
return
|
|
}
|
|
}()
|
|
|
|
conf := ssh.ClientConfig{
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
}
|
|
conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf)
|
|
if err != nil {
|
|
t.Fatalf("NewClientConn: %v", err)
|
|
}
|
|
client := ssh.NewClient(conn, chans, reqs)
|
|
defer client.Close()
|
|
|
|
if err := ForwardToAgent(client, NewKeyring()); err != nil {
|
|
t.Fatalf("ForwardToAgent: %v", err)
|
|
}
|
|
|
|
server := <-incoming
|
|
if server == nil {
|
|
t.Fatal("Unable to get server")
|
|
}
|
|
defer server.Close()
|
|
|
|
ch, chanReqs, err := server.OpenChannel(channelType, nil)
|
|
if err != nil {
|
|
t.Fatalf("OpenChannel(%q): %v", channelType, err)
|
|
}
|
|
go ssh.DiscardRequests(chanReqs)
|
|
defer ch.Close()
|
|
|
|
// Write more than the default channel receive window (2 MiB) on the
|
|
// stderr stream. If the forwarder did not drain it the remote window
|
|
// would be exhausted and this write would block forever, because no
|
|
// window-adjust messages are emitted for an extended stream that is
|
|
// never read.
|
|
const payload = 4 * 1024 * 1024
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
buf := make([]byte, 32*1024)
|
|
remaining := payload
|
|
for remaining > 0 {
|
|
n := len(buf)
|
|
if remaining < n {
|
|
n = remaining
|
|
}
|
|
w, err := ch.Stderr().Write(buf[:n])
|
|
if err != nil {
|
|
done <- err
|
|
return
|
|
}
|
|
remaining -= w
|
|
}
|
|
done <- nil
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("write to stderr: %v", err)
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("write to stderr blocked: ForwardToAgent is not draining channel.Stderr()")
|
|
}
|
|
|
|
// The agent must still be reachable on the main stream after the
|
|
// stderr traffic.
|
|
if _, err := NewClient(ch).List(); err != nil {
|
|
t.Fatalf("List after stderr flood: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestV1ProtocolMessages(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
c := NewClient(c1)
|
|
|
|
go ServeAgent(NewKeyring(), c2)
|
|
|
|
testV1ProtocolMessages(t, c.(*client))
|
|
}
|
|
|
|
func testV1ProtocolMessages(t *testing.T, c *client) {
|
|
reply, err := c.call([]byte{agentRequestV1Identities})
|
|
if err != nil {
|
|
t.Fatalf("v1 request all failed: %v", err)
|
|
}
|
|
if msg, ok := reply.(*agentV1IdentityMsg); !ok || msg.Numkeys != 0 {
|
|
t.Fatalf("invalid request all response: %#v", reply)
|
|
}
|
|
|
|
reply, err = c.call([]byte{agentRemoveAllV1Identities})
|
|
if err != nil {
|
|
t.Fatalf("v1 remove all failed: %v", err)
|
|
}
|
|
if _, ok := reply.(*successAgentMsg); !ok {
|
|
t.Fatalf("invalid remove all response: %#v", reply)
|
|
}
|
|
}
|
|
|
|
func verifyKey(sshAgent Agent) error {
|
|
keys, err := sshAgent.List()
|
|
if err != nil {
|
|
return fmt.Errorf("listing keys: %v", err)
|
|
}
|
|
|
|
if len(keys) != 1 {
|
|
return fmt.Errorf("bad number of keys found. expected 1, got %d", len(keys))
|
|
}
|
|
|
|
buf := make([]byte, 128)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return fmt.Errorf("rand: %v", err)
|
|
}
|
|
|
|
sig, err := sshAgent.Sign(keys[0], buf)
|
|
if err != nil {
|
|
return fmt.Errorf("sign: %v", err)
|
|
}
|
|
|
|
if err := keys[0].Verify(buf, sig); err != nil {
|
|
return fmt.Errorf("verify: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addKeyToAgent(key crypto.PrivateKey) error {
|
|
sshAgent := NewKeyring()
|
|
if err := sshAgent.Add(AddedKey{PrivateKey: key}); err != nil {
|
|
return fmt.Errorf("add: %v", err)
|
|
}
|
|
return verifyKey(sshAgent)
|
|
}
|
|
|
|
func TestKeyTypes(t *testing.T) {
|
|
for k, v := range testPrivateKeys {
|
|
if err := addKeyToAgent(v); err != nil {
|
|
t.Errorf("error adding key type %s, %v", k, err)
|
|
}
|
|
if err := addCertToAgentSock(v, nil); err != nil {
|
|
t.Errorf("error adding key type %s, %v", k, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func addCertToAgentSock(key crypto.PrivateKey, cert *ssh.Certificate) error {
|
|
a, b, err := netPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
agentServer := NewKeyring()
|
|
go ServeAgent(agentServer, a)
|
|
|
|
agentClient := NewClient(b)
|
|
if err := agentClient.Add(AddedKey{PrivateKey: key, Certificate: cert}); err != nil {
|
|
return fmt.Errorf("add: %v", err)
|
|
}
|
|
return verifyKey(agentClient)
|
|
}
|
|
|
|
func addCertToAgent(key crypto.PrivateKey, cert *ssh.Certificate) error {
|
|
sshAgent := NewKeyring()
|
|
if err := sshAgent.Add(AddedKey{PrivateKey: key, Certificate: cert}); err != nil {
|
|
return fmt.Errorf("add: %v", err)
|
|
}
|
|
return verifyKey(sshAgent)
|
|
}
|
|
|
|
func TestCertTypes(t *testing.T) {
|
|
for keyType, key := range testPublicKeys {
|
|
cert := &ssh.Certificate{
|
|
ValidPrincipals: []string{"gopher1"},
|
|
ValidAfter: 0,
|
|
ValidBefore: ssh.CertTimeInfinity,
|
|
Key: key,
|
|
Serial: 1,
|
|
CertType: ssh.UserCert,
|
|
SignatureKey: testPublicKeys["rsa"],
|
|
Permissions: ssh.Permissions{
|
|
CriticalOptions: map[string]string{},
|
|
Extensions: map[string]string{},
|
|
},
|
|
}
|
|
if err := cert.SignCert(rand.Reader, testSigners["rsa"]); err != nil {
|
|
t.Fatalf("signcert: %v", err)
|
|
}
|
|
if err := addCertToAgent(testPrivateKeys[keyType], cert); err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
if err := addCertToAgentSock(testPrivateKeys[keyType], cert); err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseConstraints(t *testing.T) {
|
|
// Test LifetimeSecs
|
|
var msg = constrainLifetimeAgentMsg{pseudorand.Uint32()}
|
|
lifetimeSecs, _, _, err := parseConstraints(ssh.Marshal(msg))
|
|
if err != nil {
|
|
t.Fatalf("parseConstraints: %v", err)
|
|
}
|
|
if lifetimeSecs != msg.LifetimeSecs {
|
|
t.Errorf("got lifetime %v, want %v", lifetimeSecs, msg.LifetimeSecs)
|
|
}
|
|
|
|
// Test ConfirmBeforeUse
|
|
_, confirmBeforeUse, _, err := parseConstraints([]byte{agentConstrainConfirm})
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
if !confirmBeforeUse {
|
|
t.Error("got comfirmBeforeUse == false")
|
|
}
|
|
|
|
// Test ConstraintExtensions
|
|
var data []byte
|
|
var expect []ConstraintExtension
|
|
for i := 0; i < 10; i++ {
|
|
var ext = ConstraintExtension{
|
|
ExtensionName: fmt.Sprintf("name%d", i),
|
|
ExtensionDetails: []byte(fmt.Sprintf("details: %d", i)),
|
|
}
|
|
expect = append(expect, ext)
|
|
if i%2 == 0 {
|
|
data = append(data, agentConstrainExtension)
|
|
} else {
|
|
data = append(data, agentConstrainExtensionV00)
|
|
}
|
|
data = append(data, ssh.Marshal(ext)...)
|
|
}
|
|
_, _, extensions, err := parseConstraints(data)
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
if !reflect.DeepEqual(expect, extensions) {
|
|
t.Errorf("got extension %v, want %v", extensions, expect)
|
|
}
|
|
|
|
// Test Malformed Constraint
|
|
_, _, _, err = parseConstraints([]byte{1})
|
|
if err != io.ErrUnexpectedEOF {
|
|
t.Errorf("got %v, want %v", err, io.ErrUnexpectedEOF)
|
|
}
|
|
|
|
// Test Unknown Constraint
|
|
_, _, _, err = parseConstraints([]byte{128})
|
|
if err == nil || !strings.Contains(err.Error(), "unknown constraint") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseEd25519KeyShortPanic(t *testing.T) {
|
|
msg := ssh.Marshal(ed25519KeyMsg{
|
|
Type: ssh.KeyAlgoED25519,
|
|
Pub: []byte{1, 2, 3},
|
|
Priv: []byte{1, 2, 3, 4, 5},
|
|
})
|
|
|
|
a, b, err := netPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- ServeAgent(NewKeyring(), a) }()
|
|
|
|
c := NewClient(b)
|
|
_, err = c.(*client).call(msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
keys, err := c.List()
|
|
if err != nil {
|
|
t.Fatalf("agent died: %v", err)
|
|
}
|
|
if len(keys) != 0 {
|
|
t.Error("short ed25519 key was accepted into keyring")
|
|
}
|
|
}
|