Avoiding The Needless Multiplication Of Forms
Implementing interactive features
Dr Andrew Moss2019-08-20
Part 2: Identifying sessions with MACs
The rough plan for this series of posts is to describe the different pieces of code in the front- and back-ends that enable interactivity.
- Sessions for tracking logged in users.
- The websocket and its protocol.
- Persistent storage and the cache.
- The preview service: parser and renderer.
- Javascript for the client side.
- Benchmarking and performance.
- Database storage.
After a user has been authenticated we store their details in the following struct:
type Session struct {
Sub string
Profile string
Email string
Name string
Token *oauth2.Token
Provider string
}
The first fields were retrieved from the identity provider when the user logged in. The Subfield is an opaque value that identifies the user to the service (i.e. a subscriber number), while the other three are useful for displaying their comments. The Tokenis our access code for the identity service, and the Provideris just an enumeration of identity providers.
Sessions are stored as values in a map. The keys are the login cookie that we have set in the user browser. Their raw form looks like this:
Google|12345|...binary data....
This gives us the identity provider and their opaque id (subscriber number in this case), followed by a MAC (Message Authentication Code). We can think of the MAC as being a signature that proves that we generated the cookie. Sessions are not meant to persist across server restarts so the key for the MAC is just a random 256-bit value picked when the server starts. Because the MAC is a binary stream and we want to store this key as a string we simply base-64 encode the raw form (byte-slice).
MACs are difficult to get right - subtle changes in the sequence of operations and the order of the substrings can change their security properties. All the details are in
Keying hash functions for message authentication and luckily there is already an implementation in the standard Go libraries. Use of a HMAC in Go is very simple. At the end of the callback handler (described in the
OAuth2 post) we did this:
loginKey := fmt.Sprintf("%s|%s", provider, userInfo.Sub)
encLogin := msgMac(loginKey)
This relies on a HMAC initialised to a random key. If we can't do this (e.g. /dev/random didn't start properly in the service jail) then we bail. This successfully follows the two main rules of crypto: #1 Randomize your IVs, #2
Don't be Sony.
hmacKey = make([]byte,32)
_,err := rand.Read(hmacKey)
if err!=nil {
fmt.Printf("Can't initialise the random hmac key! %s\n", err.Error())
return
}
stateHmac = hmac.New(sha256.New,hmacKey)
All we need to do to get our session key is apply the MAC and base-64 encode the result:
func msgMac(msg string) string {
stateHmac.Reset()
stateHmac.Write([]byte(msg))
mac := stateHmac.Sum(nil)
state := fmt.Sprintf("%s|%s",msg,mac)
return base64.StdEncoding.EncodeToString([]byte(state))
}
When a user logs in, this key is written as their login cookie. It makes finding the session easy:
func Find(req *http.Request) (*Session,[]byte) {
var session *Session = nil
token,err := req.Cookie("login")
if err==nil {
loginKey,ok := checkMac(token.Value)
if ok {
session, _ = sessions[string(loginKey)]
if session==nil { fmt.Printf("Error? %s %s\n", session, sessions ) }
} else {
fmt.Printf("Login %v failed mac check\n",token.Value)
}
}
if session==nil {
session = &Session{Name:"guest",provider:"none"}
}
sessionBar := session.GenerateBar()
return session, sessionBar
}
The sessionBaris the grey strip across the top of the page that allows logging in / out and shows the current state. If the cookie is valid (the MAC passed the check) and it matches a current session then we show the logged in version. In all other cases we show the logged out version and let the user login (overwriting any stale state). We do not show any intermediate states to the user, and if anything goes wrong (i.e. the cookie is stale) then diagnostics go to the logs, not back to the browser.
Checking the MAC is valid means regenerating the code from the subscriber number and provider inside the raw form of the cookie:
func checkMac(mac string) ([]byte, bool) {
raw,err := base64.StdEncoding.DecodeString(mac)
fmt.Printf("checkMac: mac %d bytes -> %d %v/%v\n", len(mac), len(raw), mac, raw)
if err!=nil {
fmt.Println("checkMac failed to base64 decode state")
return nil, false
}
firstSplit := bytes.IndexByte(raw,'|')
if firstSplit==-1 { return nil, false }
secondSplit := firstSplit + 1 + bytes.IndexByte(raw[firstSplit+1:],'|')
msg := raw[:secondSplit]
oldSig := raw[secondSplit+1:]
fmt.Printf("Checking: %s | %v as mac\n", string(msg), oldSig)
stateHmac.Reset()
stateHmac.Write([]byte(msg))
newSig := stateHmac.Sum(nil)
match := hmac.Equal(oldSig,newSig)
if !match { fmt.Printf("checkMac failed to match sig %v vs %v",oldSig,newSig) }
return msg, match
}
In the handlers for incoming GET/POST requests we can now identify the ongoing Session for a logged in user. In order to hijack a session an adversary would need to perform one of the (hopefully difficult tasks):
- Break the TSL on the connection to read the cookie in transit.
- Take control of an authenticated user's browser to read the cookie.
- Guess the random key for the HMAC.
- Forge a valid HMAC for a current session.
The difficuly of these tasks gives us a reasonable level of security, while the lookup code on a request handler is simple, just call the Findprocedure above. Next time I will describe the Websocket and its protocol...
Comments
Sign in at the top of the page to leave a comment