skip to content

In a WebSocket opening handshake, how does the server compute Sec-WebSocket-Accept, and what does that value prove?

level: middleimportance: must knowfreq 66%

answer

  1. a proof of comprehension, not identity
  2. public inputs on both sides
  3. the text as received, not decoded
  4. a constant every implementation already holds
  5. hash the pair, encode twenty bytes

basics

~10 s

The server appends the fixed GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 to the client's Sec-WebSocket-Key text, SHA-1 hashes the result and base64-encodes the digest. It proves the peer implements the WebSocket handshake - not authentication, privacy or integrity.

solid answer

~40 s

The server takes the `Sec-WebSocket-Key` value **as it arrived** — the base64 text, not the 16 bytes it decodes to — concatenates the fixed GUID `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` to it, runs SHA-1 over that string, and base64-encodes the 20-byte digest into `Sec-WebSocket-Accept` on the 101. The GUID is published in RFC 6455, so it is neither secret nor negotiated: every server uses the same one. What the answer proves is only that whatever replied genuinely implements this handshake and saw *this connection's* key, which is why the key must be freshly random per connection — a stored or replayed response cannot carry the right digest. It provides no authentication, no confidentiality and no integrity; anyone who can read the request can compute the answer.

code

pseudocode · 8 lines
pseudocode
function computeAccept(keyFieldValue):
    # keyFieldValue is the base64 TEXT as received, not the decoded bytes
    combined = keyFieldValue + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    digest   = sha1(combined)        # 20 raw bytes
    return base64(digest)            # 28 characters

# computeAccept("dGhlIHNhbXBsZSBub25jZQ==")
#   -> "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

go deeper

for a junior

Recall that the client sends Sec-WebSocket-Key and the server answers Sec-WebSocket-Accept, and that the accept value is derived from the key rather than chosen freely.

for a middle

Explain the function end to end: base64 key text plus the published GUID, SHA-1, base64 again - and say plainly that it proves comprehension and freshness, not identity.

for a senior

Demonstrate the limit of the mechanism: the constant is public, the key is readable, so the digest defends against a stale or unaware responder, never against a reader or an attacker on the path.

for a principal

The angle worth having is why a protocol would spend a round of hashing on a property this narrow: it buys safe protocol switching without adding a security dependency the transport already owns.

## The computation, step by step 1. Take the value of the client's `Sec-WebSocket-Key` field **exactly as it arrived on the wire**: the base64 text, not the 16 bytes it decodes to. This is the single most common implementation error. 2. Concatenate the fixed GUID `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` directly onto it, with no separator. 3. Run **SHA-1** over that string, producing a 20-byte digest. 4. **base64-encode** those 20 bytes, giving a 28-character value. 5. Return it as `Sec-WebSocket-Accept` in the `101 Switching Protocols` response. With the specification's own worked example, the key `dGhlIHNhbXBsZSBub25jZQ==` produces the accept value `s3pPLMBiTxaQ9kYGzzhZRbK+xOo=`. The client, which knows the key it sent, recomputes the same function and compares. ## Why a public constant is not a contradiction Candidates often assume a constant baked into a published specification must be a shared secret that leaked, and conclude the mechanism is broken. It is not a secret and was never meant to be one. Its job is narrower: - It makes the correct answer **impossible to produce by accident**. An endpoint that merely echoes fields, or one that answers every request from a stored response, will not return the right 28 characters for this particular key. - Combined with the requirement that the key is **fresh random bytes per connection**, it makes a **replayed** response detectable: a response captured from an earlier exchange carries a digest bound to that exchange's key, not this one's. - It makes the handshake **self-describing**. A server that returns the right value has demonstrably read the specification and is about to speak the protocol, so the client can safely stop treating the connection as HTTP. - It costs nothing to deploy. Because the constant ships in the specification rather than in configuration, two implementations that have never met agree on the computation without exchanging anything beforehand. That is the whole claim. The digest is a proof of comprehension, not a proof of identity, and it is settled in the same single round trip that opens the connection. ## What the Key/Accept pair does not give you | Property | Provided by the digest? | Where it actually comes from | |---|---|---| | Proof the peer implements this handshake | **Yes** | The digest, recomputed and compared by the client | | Detection of a replayed or stored response | **Yes**, given a fresh random key | The per-connection nonce plus the digest | | Authentication of the user or the client | No | Credentials on the connection, a separate subject | | Confidentiality of the request | No | TLS, when the `wss:` scheme is used | | Integrity against an on-path modifier | No | TLS, when the `wss:` scheme is used | Anyone able to read the request can read the key and compute the answer, because the algorithm and the constant are both public. Saying so out loud is the part interviewers listen for. ## SHA-1 here is not a security claim SHA-1 is weak against collision attacks, and a candidate who knows that often volunteers it as a flaw in the handshake. It is not, because **nothing here depends on collision or preimage resistance**. Both ends compute the same fixed function over the same public input and compare the results; an attacker who could forge a colliding digest would have gained nothing that reading the key does not already give them. Nor can an implementation substitute a stronger hash: the algorithm and the GUID are fixed by the specification, and a server that changes either simply fails every handshake. ## What the client does with the answer The client recomputes the digest from the key it sent and compares it with the value in `Sec-WebSocket-Accept`. If the field is absent, or present with a different value, the handshake has failed and the client must not treat the following bytes as protocol data. That validation step — and what it protects against — is where this question usually goes next. ## In the interview A strong answer states four things in order: **the input** (the base64 key text plus the published GUID), **the function** (SHA-1 then base64), **the direction** (the client sends the key, the server answers the digest), and **the limit** (it proves comprehension and freshness, nothing more). Getting the direction backwards, or promoting the digest to a security feature, are the two failures that carry.

  • Is the Sec-WebSocket-Key decoded to its 16 bytes before hashing?
    No. The server hashes the base64 text exactly as it appeared in the field. Decoding first is a classic implementation bug: it produces a well-formed but wrong digest, and every conforming client then fails the handshake.
  • If the key is not a secret, why must it be freshly random on every connection?
    Because freshness is the only thing the digest binds. A random per-connection nonce means a response captured or cached from an earlier handshake carries the wrong digest for this one, so the client detects it instead of treating stale bytes as a live socket.
  • Could a server use a stronger hash than SHA-1 for the accept value?
    No. The algorithm and the GUID are fixed by the specification, so changing either just breaks every handshake. It also gains nothing: the digest's security properties are not what it is for, and its inputs are public on both sides.

A lock keeper who answers 'gate three, ready' has not proved who they are - only that they heard your exact instruction and speak the same signalling code. Anyone standing on the towpath hears both halves of that exchange.

saying these in an interview costs you the question

  • Calls Sec-WebSocket-Accept a signature that authenticates the server
  • Thinks the GUID is a secret shared between client and server
  • Says SHA-1's collision weakness makes the handshake insecure
  • Decodes the key to 16 bytes before hashing, producing a wrong digest
  • Believes the digest protects the request from someone reading it
  • Has the client computing the accept value and the server checking it