nandi/frqpublic Fork 0
e505dedb6ac7169a867e5e9f7732e997d8a003f9
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

SASL, and a screen that stops lying about it e50cbc7 · on e505dedb6ac7169a867e5e9f7732e997d8a003f9 · nandi · 15h ago
thandshake.nim · 137 lines · 5.4 KBNim Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 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
 67
 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
132
133
134
135
136
137
## CAP negotiation and the SASL payload. No network: every case here is a
## line in and lines out.

import std/[base64, json, sets, strutils, unittest]
import frq/[ircparse, atproto, handshake]

proc caps0(): HashSet[string] = initHashSet[string]()

proc guest(): Session = Session(kind: skNone)
proc signedIn(): Session =
  Session(kind: skPdsSession, did: "did:plc:abc", accessJwt: "jwt-123",
          pds: "https://pds.example")

suite "base64url":
  test "unpadded, and URL-safe":
    check b64url("hello") == "aGVsbG8"
    check '=' notin b64url("any")
    check '+' notin b64url("\xfb\xff")
    check '/' notin b64url("\xfb\xff")

  test "round-trips":
    for s in ["", "a", "ab", "abc", "{\"nonce\":\"x\"}", "😀"]:
      check b64urlDecode(b64url(s)) == s

  test "garbage decodes to nothing rather than throwing":
    check b64urlDecode("!!!not base64!!!") == ""

suite "nonceOf":
  test "the nonce out of a challenge":
    let challenge = b64url($(%*{"session_id": "s", "nonce": "N123"}))
    check nonceOf(challenge) == "N123"
  test "a challenge with no nonce":
    check nonceOf(b64url($(%*{"session_id": "s"}))) == ""
  test "a challenge that is not JSON":
    check nonceOf(b64url("not json")) == ""

suite "saslResponse":
  test "a pds-session carries the DID, the token, the PDS and the nonce":
    let payload = parseJson(b64urlDecode(saslResponse(signedIn(), "N1")))
    check payload["method"].getStr() == "pds-session"
    check payload["did"].getStr() == "did:plc:abc"
    check payload["signature"].getStr() == "jwt-123"
    check payload["pds_url"].getStr() == "https://pds.example"
    # Echoed back so the token cannot be replayed at another server.
    check payload["challenge_nonce"].getStr() == "N1"

  test "a web-token carries only the token, with the DID left empty":
    # The server looks the DID up in its own store; guessing it would be
    # wrong more often than not.
    let s = Session(kind: skWebToken, token: "tok")
    let payload = parseJson(b64urlDecode(saslResponse(s, "N1")))
    check payload["method"].getStr() == "web-token"
    check payload["signature"].getStr() == "tok"
    check payload["did"].getStr() == ""

suite "saslLines":
  test "a short payload is one line":
    check saslLines("abc") == @["AUTHENTICATE abc"]

  test "one that lands exactly on the boundary gets a bare + after it":
    # Or the server waits for a continuation that is not coming.
    let exact = "x".repeat(saslChunk)
    let got = saslLines(exact)
    check got.len == 2
    check got[1] == "AUTHENTICATE +"

  test "a longer one is split":
    check saslLines("x".repeat(saslChunk + 5)).len == 2

suite "step: CAP":
  test "LS asks for what it can use":
    let m = parseLine(":s CAP * LS :message-tags server-time account-tag echo-message")
    let got = step(guest(), caps0(), m)
    check got.send.len == 1
    check got.send[0].startsWith("CAP REQ :")
    for c in ["message-tags", "server-time", "account-tag", "echo-message"]:
      check c in got.send[0]

  test "it asks only for what was offered":
    let m = parseLine(":s CAP * LS :server-time")
    check step(guest(), caps0(), m).send[0] == "CAP REQ :server-time"

  test "nothing on offer ends CAP rather than requesting nothing":
    check step(guest(), caps0(), parseLine(":s CAP * LS :")).send == @["CAP END"]

  test "a guest does not ask for sasl even when it is offered":
    # It would have nothing to answer the challenge with.
    let m = parseLine(":s CAP * LS :sasl server-time")
    check "sasl" notin step(guest(), caps0(), m).send[0]

  test "a signed-in session does":
    let m = parseLine(":s CAP * LS :sasl server-time")
    check "sasl" in step(signedIn(), caps0(), m).send[0]

  test "ACK with sasl starts the exchange":
    let m = parseLine(":s CAP nick ACK :sasl server-time")
    let got = step(signedIn(), caps0(), m)
    check got.send == @["AUTHENTICATE ATPROTO-CHALLENGE"]
    check "sasl" in got.caps
    check "server-time" in got.caps

  test "ACK without sasl ends CAP":
    let m = parseLine(":s CAP nick ACK :server-time")
    check step(guest(), caps0(), m).send == @["CAP END"]

  test "NAK ends CAP rather than hanging":
    check step(guest(), caps0(), parseLine(":s CAP nick NAK :sasl")).send ==
      @["CAP END"]

suite "step: AUTHENTICATE":
  test "a challenge is answered with the payload":
    let challenge = b64url($(%*{"nonce": "N9"}))
    let got = step(signedIn(), caps0(), parseLine("AUTHENTICATE " & challenge))
    check got.send.len == 1
    let payload = parseJson(b64urlDecode(got.send[0]["AUTHENTICATE ".len .. ^1]))
    check payload["challenge_nonce"].getStr() == "N9"

  test "a bare + is not a challenge":
    check step(signedIn(), caps0(), parseLine("AUTHENTICATE +")).send.len == 0

suite "step: the outcome":
  test "903 ends CAP so registration can proceed":
    check step(signedIn(), caps0(), parseLine(":s 903 n :ok")).send == @["CAP END"]

  test "904 ends CAP too rather than leaving the client hanging":
    # Refused is an outcome; the caller reports it and carries on as a guest.
    for code in ["904", "905", "906"]:
      check step(signedIn(), caps0(), parseLine(":s " & code & " n :no")).send ==
        @["CAP END"]

suite "dpopNonce":
  test "the relayed nonce":
    check dpopNonce(parseLine(":s NOTICE n :DPOP_NONCE abc123")) == "abc123"
  test "an ordinary notice is not one":
    check dpopNonce(parseLine(":s NOTICE n :hello")) == ""
  test "nor is anything else":
    check dpopNonce(parseLine(":s PRIVMSG #c :DPOP_NONCE x")) == ""