Practical Security II: The Mafia IRC Bot (WIP)

To demonstrate secure distributed programming in Monte, let’s take the mafia game code developed earlier and make it into an IRC bot.

The mafiabot.mt module begins by importing the mafia module, an irc/client library, and the same modules for dealing with entropy that we saw before:

1
2
3
4
5
6
import "mafia" =~ [=> makeMafia :DeepFrozen]
import "irc/client" =~ [=> makeIRCClient :DeepFrozen,
                        => connectIRCClient :DeepFrozen]
import "lib/entropy/entropy" =~ [=> makeEntropy :DeepFrozen]
import "lib/entropy/pcg" =~ [=> makePCG :DeepFrozen]
exports (main)

The main entry point is provided with a number of powerful references as named arguments:

  • To seed our random number generator, we use currentRuntime to get a source of true randomness, i.e. secure entropy.
  • To give makeIRCService access to TCP/IP networking and event scheduling, we use makeTPC4ClientEndPoint, getAddrInfo, and Timer.
192
193
194
195
196
197
198
199
200
201
202
def main(argv,
         => makeTCP4ClientEndpoint,
         => Timer,
         => currentRuntime,
         => getAddrInfo) as DeepFrozen:
    def [_, seed] := currentRuntime.getCrypt().makeSecureEntropy().getEntropy()
    def rng := makeEntropy(makePCG(seed, 0))
    def [hostname] := argv
    def irc := makeIRCService(makeTCP4ClientEndpoint, getAddrInfo, Timer,
                              hostname)
    irc.connect(makeMafiaBot(rng))

We can go ahead and run this code from a file by using the monte commandline tool:

monte eval mafiabot.mt chat.freenode.net

Everything after the source filename is passed to main in argv as a list of strings.

Networking

Unlike many other contemporary programming languages, Monte does not need an additional networking library to provide solid primitive and high-level networking operations. This is because Monte was designed to handle networking as easily as any other kind of input or output.

 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def makeIRCService(makeTCP4ClientEndpoint, getAddrInfo, Timer,
                   hostname :Str) as DeepFrozen:
    def port := 6667  # TODO: named arg with default value

    return object IRC:
        to _printOn(out):
            out.print(`IRC($hostname)`)

        to connect(handler):
            def client := makeIRCClient(handler, Timer)

            def addrs := getAddrInfo(b`$hostname`, b``)
            return when (addrs) ->
                def choices := [
                    for addr in (addrs)
                    ? (addr.getFamily() == "INET" &&
                       addr.getSocketType() == "stream") addr.getAddress()]
                def [address] + _ := choices
                def ep := makeTCP4ClientEndpoint(address, port)
                connectIRCClient(client, ep)
                client

Distributed Systems

Monte comes with builtin explicit parallelism suitable for scaling to arbitrary numbers of processes or machines, and a well-defined concurrency system that simplifies and streamlines the task of writing event-driven code.

Monte has one concurrent operation. Monte permits messages to be passed as eventual sends. An eventually-sent message will be passed to the target object at a later time, generating a promise which can have more messages sent to it. Unlike similar mechanisms in Twisted, Node.js, etc., Monte builds promises and eventual sending directly into the language and runtime, removing the need for extraneous libraries.

Monte also has a single primitive for combining isolation and parallelism, the vat. Each vat isolates a collection of objects from objects in other vats. Each eventual send in a vat becomes a distinct turn of execution, and vats execute concurrently with one another. During a turn, a vat delivers a single queued send, which could result in more sends being queued up for subsequent turns.

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def makeChannelVow(client, name) as DeepFrozen:
    "Return a vow because say() won't work until we have joined."
    def [wait, done] := Ref.promise()
    var waitingFor :NullOk[Set[Str]]:= null

    object chan:
        to _printOn(out):
            out.print(`<channel $name>`)
        to getName():
            return name
        to hasJoined():
            return client.hasJoined(name)
        to say(message) :Void:
            client.say(name, message)
        to getUsers(notReady):
            return client.getUsers(name, notReady)
        to waitFor(them :Set[Str]):
            waitingFor := them
            return wait
        to notify():
            if (waitingFor != null):
                escape oops:
                    def present := chan.getUsers(oops).getKeys().asSet()
                    traceln("notify present:", present, waitingFor,
                            waitingFor - present)
                    if ((waitingFor - present).size() == 0):
                        waitingFor := null
                        done.resolve(present)
        to tell(whom, what, notInChannel):
            if (chan.getUsers(notInChannel).contains(whom)):
                client.say(whom, what)
            else:
                notInChannel(`cannot tell $whom: not in $name`)
        to part(message):
            client.part(name, message)
    return when(chan.hasJoined()) ->
        chan

Principle of Least Authority

Straightforwad object-oriented design results in each object having the least authority it needs:

  • makeIRCService provides the full range of IRC client behavior
  • makeChannelVow provides access to one channel
  • makeModerator encapsulates the play of one game
  • makePlayer represents the role of one player in one game
  • makeMafiaBot starts games on request, routes messages to the relevant moderator during game play, and disposes of moderators when games end.

Even if one of these components is buggy or compromised, its ability to corrupt the system is limited to using the capabilities in its static scope.

Contrast this with traditional identity-based systems, where programs execute with all privileges granted to a user or role. In such a system, any compromise lets the attacker do anything that the user could do. A simple game such as solitaire executes with all authority necessary to corrupt, exfiltrate, or ransom the user’s files.

With object capability disciplne, when the time comes for a security inspection, we do not have to consider the possibility that any compromise in any part of our program leaves the whole system wide open in this way. Each component in the system can be reviewed independently and auditing a system for security becomes cost-effective to an extent that is infeasible with other approaches [1].

 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def makeModerator(playerNames :Set[Str], rng,
                  chan :Near, mafiaChan) as DeepFrozen:
    def [=> game, => mafiosos] := makeMafia(playerNames, rng)
    var night0 := true

    def makePlayer(me :Str):
        return object player:
            to _printOn(out):
                out.print(`<player $me>`)
            to voteFor(nominee :Str):
                try:
                    game.vote(me, nominee)
                catch _:
                    # nominee is not (any longer) a player
                    return
                chan.say(game.advance())

    def toPlayer := [for nick in (playerNames) nick => makePlayer(nick)]

    return object moderator:
        to _printOn(out):
            out.print(`<moderator in $chan>`)

        to begin():
            # Night 0
            chan.say(`$game`)
            when (mafiaChan) ->
                escape notHere:
                    for maf in (mafiosos):
                        chan.tell(
                            maf, `You're a mafioso in $chan.`, notHere)
                        chan.tell(
                            maf, `Join $mafiaChan to meet the others.`, notHere)
                traceln("waiting for", mafiosos, "in", mafiaChan)
                when (mafiaChan.waitFor(mafiosos)) ->
                    traceln("done waiting for", mafiosos)
                    night0 := false
                    # Morning of day 1...
                    chan.say(game.advance())

        to said(who :Str, message :Str) :Bool:
            "Return true to contine, false if game over."
            mafiaChan.notify()
            traceln("notifying", mafiaChan)
            if (night0):
                return true
            if (message =~ `lynch @whom!`):
                escape notPlaying:
                    def p := moderator.getPlayer(who, notPlaying)
                    p.voteFor(whom)
                    traceln("lynch", who, whom)

                    if (game.getWinner() =~ winner ? (winner != null)):
                        moderator.end()

            return game.getWinner() == null

        to getPlayer(name, notPlaying):
            return toPlayer.fetch(name, notPlaying)

        to end():
            chan.say(`$game`)
            chan.part("Good game!")
            mafiaChan.part("bye bye")

Note the way makeMafiaBot provides a secret channel for the mafiosos to collude at night:

133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def makeMafiaBot(rng) as DeepFrozen:
    def nick := "mafiaBot"
    def chanMod := [].asMap().diverge()
    def keys := [].asMap().diverge()

    return object mafiaBot:
        to getNick():
            return nick

        to loggedIn(client):
            return null

        to privmsg(client, user, channel, message):
            # traceln("mafiaBot got", message, "on", channel, "from", user,
            #         "channels", chanMod.getKeys())
            def who := user.getNick()

            if (message =~ `join @dest` &&
                channel == nick &&
                !keys.contains(dest)):
                mafiaBot.join(client, who, dest)
            else if (message == "start" &&
                     !keys.contains(channel)):
                when(def chan := makeChannelVow(client, channel)) ->
                    mafiaBot.startGame(client, chan, channel)
            else if (chanMod.snapshot() =~ [(channel) => m] | _):
                if (!m.said(who, message)):
                    def chKey := keys[channel]
                    chanMod.removeKey(channel)
                    chanMod.removeKey(chKey)
                    keys.removeKey(channel)
                    keys.removeKey(chKey)
                    traceln("removed", channel, chKey)

        to join(client, who :Str, channel :Str):
            when(client.hasJoined(channel)) ->
                client.say(channel, `Thank you for inviting me, $who.`)
                client.say(channel, `Say "start" to begin.`)

        to startGame(client, chan :Near, channel :Str):
            def secret := `$channel-${rng.nextInt(2 ** 32)}`
            def secretChan := makeChannelVow(client, secret)
            escape notReady:
                def users := chan.getUsers(notReady)
                def playerNames := [
                    for name => _ in (users)
                    ? (name != nick)
                    # @chanop -> chanop
                    (if (name =~ `@@@op`) { op } else { name })]
                traceln("players:", playerNames, users)

                def m := makeModerator(playerNames.asSet(), rng,
                                       chan, secretChan)
                chanMod[channel] := chanMod[secret] := m
                keys[channel] := secret
                keys[secret] := channel
                m.begin()

Notes

[1]As documented in the DarpaBrowser report