Skip to content

Commit

Permalink
auth: Optionally not send UID with external auth
Browse files Browse the repository at this point in the history
Due to mismatch between UID in a user-namespace
and out-of-band credential acquired by server
on another user-namespace refrain from sending UID
with external authentication by default
to keep compatibility still fallback to sending UID
if it fails

godbus#345
  • Loading branch information
😎Mostafa Emami committed Jan 2, 2023
1 parent a852926 commit e026873
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 4 deletions.
9 changes: 5 additions & 4 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ type Auth interface {
}

// Auth authenticates the connection, trying the given list of authentication
// mechanisms (in that order). If nil is passed, the EXTERNAL and
// DBUS_COOKIE_SHA1 mechanisms are tried for the current user. For private
// connections, this method must be called before sending any messages to the
// mechanisms (in that order). If nil is passed, the EXTERNAL with empty authorization identity
// (authentication done with what-ever uid server can derive from out-of-band credentials),
// EXTERNAL with uid and DBUS_COOKIE_SHA1 mechanisms are tried for the current user.
// For private connections, this method must be called before sending any messages to the
// bus. Auth must not be called on shared connections.
func (conn *Conn) Auth(methods []Auth) error {
if methods == nil {
uid := strconv.Itoa(os.Geteuid())
methods = []Auth{AuthExternal(uid), AuthCookieSha1(uid, getHomeDir())}
methods = []Auth{AuthExternal(""), AuthExternal(uid), AuthCookieSha1(uid, getHomeDir())}
}
in := bufio.NewReader(conn.transport)
err := conn.transport.SendNullByte()
Expand Down
52 changes: 52 additions & 0 deletions conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import (
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"sync"
"syscall"
"testing"
"time"
)
Expand Down Expand Up @@ -78,6 +81,55 @@ func TestConnectSystemBus(t *testing.T) {
}
}

func TestConnectBusInDifferentUserNamespace(t *testing.T) {
oldConn, err := SessionBus()
if err != nil {
t.Error(err)
}
if err = oldConn.Close(); err != nil {
t.Fatal(err)
}
if oldConn.Connected() {
t.Fatal("Should be closed")
}
newConn, err := SessionBus()
if err != nil {
t.Error(err)
}
if newConn == oldConn {
t.Fatal("Should get a new connection")
}

cmd := exec.Command("/bin/bash")

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUSER,
UidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: os.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: os.Getgid(),
Size: 1,
},
},
}

if err := cmd.Run(); err != nil {
fmt.Printf("Error running the exec.Command - %s\n", err)
os.Exit(1)
}
}

func TestSend(t *testing.T) {
bus, err := ConnectSessionBus()
if err != nil {
Expand Down
258 changes: 258 additions & 0 deletions test
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package dbus

import (
"bufio"
"bytes"
"errors"
"io"
"os"
"strconv"
)

// AuthStatus represents the Status of an authentication mechanism.
type AuthStatus byte

const (
// AuthOk signals that authentication is finished; the next command
// from the server should be an OK.
AuthOk AuthStatus = iota

// AuthContinue signals that additional data is needed; the next command
// from the server should be a DATA.
AuthContinue

// AuthError signals an error; the server sent invalid data or some
// other unexpected thing happened and the current authentication
// process should be aborted.
AuthError
)

type authState byte

const (
waitingForData authState = iota
waitingForOk
waitingForReject
)

// Auth defines the behaviour of an authentication mechanism.
type Auth interface {
// Return the name of the mechanism, the argument to the first AUTH command
// and the next status.
FirstData() (name, resp []byte, status AuthStatus)

// Process the given DATA command, and return the argument to the DATA
// command and the next status. If len(resp) == 0, no DATA command is sent.
HandleData(data []byte) (resp []byte, status AuthStatus)
}

// Auth authenticates the connection, trying the given list of authentication
// mechanisms (in that order). If nil is passed, the EXTERNAL with empty authorization identity
// (authentication done with what-ever uid server can derive from out-of-band credentials),
// EXTERNAL with uid and DBUS_COOKIE_SHA1 mechanisms are tried for the current user.
// For private connections, this method must be called before sending any messages to the
// bus. Auth must not be called on shared connections.
func (conn *Conn) Auth(methods []Auth) error {
if methods == nil {
uid := strconv.Itoa(os.Geteuid())
methods = []Auth{AuthExternal(""), AuthExternal(uid), AuthCookieSha1(uid, getHomeDir())}
}
in := bufio.NewReader(conn.transport)
err := conn.transport.SendNullByte()
if err != nil {
return err
}
err = authWriteLine(conn.transport, []byte("AUTH"))
if err != nil {
return err
}
s, err := authReadLine(in)
if err != nil {
return err
}
if len(s) < 2 || !bytes.Equal(s[0], []byte("REJECTED")) {
return errors.New("dbus: authentication protocol error")
}
s = s[1:]
for _, v := range s {
for _, m := range methods {
if name, _, status := m.FirstData(); bytes.Equal(v, name) {
var ok bool
err = authWriteLine(conn.transport, []byte("AUTH"), v)
if err != nil {
return err
}
switch status {
case AuthOk:
ok, err = conn.tryAuth(m, waitingForOk, in)
case AuthContinue:
ok, err = conn.tryAuth(m, waitingForData, in)
default:
panic("dbus: invalid authentication status")
}
if err != nil {
return err
}
if ok {
if conn.transport.SupportsUnixFDs() {
err = authWriteLine(conn, []byte("NEGOTIATE_UNIX_FD"))
if err != nil {
return err
}
line, err := authReadLine(in)
if err != nil {
return err
}
switch {
case bytes.Equal(line[0], []byte("AGREE_UNIX_FD")):
conn.EnableUnixFDs()
conn.unixFD = true
case bytes.Equal(line[0], []byte("ERROR")):
default:
return errors.New("dbus: authentication protocol error")
}
}
err = authWriteLine(conn.transport, []byte("BEGIN"))
if err != nil {
return err
}
go conn.inWorker()
return nil
}
}
}
}
return errors.New("dbus: authentication failed")
}

// tryAuth tries to authenticate with m as the mechanism, using state as the
// initial authState and in for reading input. It returns (true, nil) on
// success, (false, nil) on a REJECTED and (false, someErr) if some other
// error occurred.
func (conn *Conn) tryAuth(m Auth, state authState, in *bufio.Reader) (bool, error) {
for {
s, err := authReadLine(in)
if err != nil {
return false, err
}
switch {
case state == waitingForData && string(s[0]) == "DATA":
if len(s) != 2 {
err = authWriteLine(conn.transport, []byte("ERROR"))
if err != nil {
return false, err
}
continue
}
data, status := m.HandleData(s[1])
switch status {
case AuthOk, AuthContinue:
if len(data) != 0 {
err = authWriteLine(conn.transport, []byte("DATA"), data)
if err != nil {
return false, err
}
}
if status == AuthOk {
state = waitingForOk
}
case AuthError:
err = authWriteLine(conn.transport, []byte("ERROR"))
if err != nil {
return false, err
}
}
case state == waitingForData && string(s[0]) == "REJECTED":
return false, nil
case state == waitingForData && string(s[0]) == "ERROR":
err = authWriteLine(conn.transport, []byte("CANCEL"))
if err != nil {
return false, err
}
state = waitingForReject
case state == waitingForData && string(s[0]) == "OK":
if len(s) != 2 {
err = authWriteLine(conn.transport, []byte("CANCEL"))
if err != nil {
return false, err
}
state = waitingForReject
} else {
conn.uuid = string(s[1])
return true, nil
}
case state == waitingForData:
err = authWriteLine(conn.transport, []byte("ERROR"))
if err != nil {
return false, err
}
case state == waitingForOk && string(s[0]) == "OK":
if len(s) != 2 {
err = authWriteLine(conn.transport, []byte("CANCEL"))
if err != nil {
return false, err
}
state = waitingForReject
} else {
conn.uuid = string(s[1])
return true, nil
}
case state == waitingForOk && string(s[0]) == "DATA":
err = authWriteLine(conn.transport, []byte("DATA"))
if err != nil {
return false, nil
}
case state == waitingForOk && string(s[0]) == "REJECTED":
return false, nil
case state == waitingForOk && string(s[0]) == "ERROR":
err = authWriteLine(conn.transport, []byte("CANCEL"))
if err != nil {
return false, err
}
state = waitingForReject
case state == waitingForOk:
err = authWriteLine(conn.transport, []byte("ERROR"))
if err != nil {
return false, err
}
case state == waitingForReject && string(s[0]) == "REJECTED":
return false, nil
case state == waitingForReject:
return false, errors.New("dbus: authentication protocol error")
default:
panic("dbus: invalid auth state")
}
}
}

// authReadLine reads a line and separates it into its fields.
func authReadLine(in *bufio.Reader) ([][]byte, error) {
data, err := in.ReadBytes('\n')
if err != nil {
return nil, err
}
data = bytes.TrimSuffix(data, []byte("\r\n"))
return bytes.Split(data, []byte{' '}), nil
}

// authWriteLine writes the given line in the authentication protocol format
// (elements of data separated by a " " and terminated by "\r\n").
func authWriteLine(out io.Writer, data ...[]byte) error {
buf := make([]byte, 0)
for i, v := range data {
buf = append(buf, v...)
if i != len(data)-1 {
buf = append(buf, ' ')
}
}
buf = append(buf, '\r')
buf = append(buf, '\n')
n, err := out.Write(buf)
if err != nil {
return err
}
if n != len(buf) {
return io.ErrUnexpectedEOF
}
return nil
}

0 comments on commit e026873

Please sign in to comment.