The debug-me protocol is a series of messages, exchanged between the two participants, known as the user and the developer.
The messages are serialized as JSON in debug-me log files, and protocol buffers are used when sending the messages over the wire. We won't go into the full details here. See Types.hs for the data types that JSON serialization instances are derived from, and ProocolBuffers.hs for the protocol buffers format. There is also a simple framing protocol used for communicating over websockets; see WebSockets.hs.
The Activity type is the main message type. The user sends Activity Seen messages, and the developer responds with Activity Entered. There are also Control messages, which can be sent by either party at any time, and do not affect IO to the console.
Activity Seen and Activity Entered messages have a prevActivity field, which points to the Hash of a previous Activity either Seen or Entered. There is also a prevEntered field, which points to the Hash of the most recent Activity Entered. (prevActivity is Nothing for the first Activity Seen, and prevEntered is Nothing until the developer enters something.) So a chain of messages is built up.
(The exact details about how objects are hashed is not described here; see Hash.hs for the implementation. Note that the JSON strings are not directly hashed (to avoid tying hashing to JSON serialization details), instead the values in the data types are hashed.)
The user and developer have different points of view. For example, the developer could send an Activity Entered at the same time the user is sending an Activity Seen. It's not clear in which order these two Activities occurred -- in fact they occurred in different orders in different places -- and so the user and developer will disagree about it.
Since the goal of debug-me is to produce a proof of the sequence of events that occurred in a session, that is a problem. Perhaps the developer was entering "y" in response to "Display detailed reactor logs?" at the same time that a new "Vent core to atmosphere?" question was being displayed! The debug-me protocol is designed to prevent such conflicts of opinion.
The user only accepts a new Activity Entered when it meets one of these requirements:
- The Activity Entered has as its prevActivity the last Activity (Entered or Seen) that the user accepted.
The Activity Entered has as its prevActivity an older Activity that the user accepted, and its echoData matches the concacenation of every Activity Seen after the prevActivity, up to the most recent Activity Seen.
(This allows the developer to enter a command quickly without waiting for each letter to echo back to them.)
An Activity Entered must also have as its prevEntered field the hash of the last Activity Entered that was accepted, unless there have been none yet.
When an Activity Entered does not meet these rules, the user sends back a EnteredRejected message to let the developer know the input was not allowed.
The developer also checks the prevActivity of Activity Seen messages it receives from the user, to make sure that it's receiving a valid chain of messages. The developer accepts a new Activity Seen when either:
- The Activity Seen has a prevActivity that points to the last Activity Seen that the developer accepted.
- The Activity Seen has as its prevActivity an Activity Entered that the developer generated, after the last Activity Seen that the developer accepted.
(The developer does not check the prevEntered field of Activity Seen, however, the user should set it. When there are multiple developers, this helps one developer know when the user has accepted an Activity Entered from another developer.)
session startup
At the start of the debug-me session, an Ed25519 session key pair are generated by the user. The first message in the protocol is the user sending their session pubic key in a Control message containing their SessionKey. The second message is an Activity Seen.
The developer also has a Ed25519 session key pair. Before the developer can enter anything, they must send a SessionKey message with their session key, and it must be accepted by the user. The developer must have a gpg private key, which is used to sign their session key. (The user may have a gpg private key, which may sign their session key if available, but this is optional.) The user will reject session keys that are not signed by a gpg key or when the gpg key is not one they trust. The user sends a SessionKeyAccepted/SessionKeyRejected control message to indicate if they accepted the developer's key or not.
Each message in the debug-me session is signed by the party that sends it, using their session key. The hash of a message includes its signature, so the activity chain proves who sent a message, and who sent the message before it, etc.
Note that there could be multiple developers, in which case each will send their session key before being able to do anything except observe the debug-me session.
The prevActivity and prevEntered hashes are actually not included in the data sent across the wire. They are left out to save space, and get added back in by the receiver. The receiver uses the signature of the message to tell when it's found the right hashes to add back in.
protocol versioning
The SessionKey control messages include a field for the protocol version. Since the user starts by sending a SessionKey, the included version specifies the protocol version used by the rest of the protocol. When developers connect, their clients will need to check that version, and avoid sending any messages using features from a later version.
The current protocol version is "1".
Hi Joey,
I looked through http://source.debug-me.branchable.com/?p=source.git;a=blob;f=Hash.hs;hb=HEAD and since this probably scurity-relevant, allow me to be nitpicky:
instance Hashable v => Hashable (Maybe v) where hash Nothing = hash () hash (Just v) = hash v
will hash the distinct values
Just ()
andNothing
identically. Maybe you don't have anyMaybe ()
type around, but in that case you should maybe document that requirement.Thanks for that review. That would indeed be bad. To avoid that potential problem, I've specialized the instance to
Hashable (Maybe Hash)
, which is the only Maybe value that currently needs to be hashed.Still not good, I think, as the instance
Hashable Hash
hashash = id
, soand we have a collision at type
Maybe Hash
.What would work is to do the same that
Hashable []
does, i.e. has the hash again:So the problem comes from the hash "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", -- if that's intended to be a
Maybe Hash
that's the hash of aByteString
, we can't tell if it was produced by hashingNothing
, or hashingJust (mempty :: ByteString)
Double hashing would avoid this ambiguity, but it does also break backwards compatability of the debug-me protocol and logs. It's still early enough to perhaps do that without a great deal of bother, but it's not desirable.
debug-me does not appear to be actually affected by this currently. The only
Maybe Hash
in debug-me is used for a hash of values of typeActivity
andEntered
, not the hash of aByteString
. So, as far as the debug-me protocol goes, the above hash value is unambiguously the hash ofNothing
; there's noActivity
orEntered
that hashes to that value. (Barring of course, a cryptographic hash collision which would need SHA2 to be broken to be exploited.)So, I'd like to clean this up, to avoid any problems creeping in if a
Maybe Hash
got used for the hash of aByteString
. But, I don't feel it's worth breaking backwards compatibility for.(I tried adding a phantom type to Hash, so the instance could be only for
Maybe (Hash Activity)
, but quickly ran into several complications.)What I've done is fixed the instance to work like you suggested, but kept the old function as
hashOfMaybeUnsafe
and used it where necessary. This way, anything new will use the fixed instance and we don't break back-compat.