Types
ScramState = object clientNonce*: string clientFirstBare*: string serverSignature*: array[32, byte] gs2Header*: string ## GS2 header: "n,," (no channel binding), "y,," (channel binding supported ## but not negotiated โ downgrade-detection signal), or ## "p=tls-server-end-point,," (channel binding in use). channelBindingData*: seq[byte] ## Channel binding data (empty for non-PLUS)
- Intermediate state for SCRAM-SHA-256 authentication handshake.
Consts
DefaultMaxScramIterations = 10000000
- Default cap on the server-requested SCRAM iteration count. PBKDF2 runs synchronously on the event loop, so an unbounded count would let a malicious server pin the process; 10M is far above realistic settings.
oidRsaPss = [42'u, 0x00000086, 0x00000048, 0x00000086, 0x000000F7, 0x0000000D, 0x00000001, 0x00000001, 0x0000000A]
Procs
proc computeTlsServerEndpoint(certDer: openArray[byte]): seq[byte] {....raises: [], tags: [RootEffect], forbids: [].}
- RFC 5929 ยง4 tls-server-end-point: hash follows the cert's signatureAlgorithm. SHA-384/SHA-512 preserved; MD5/SHA-1/unknown/parse-failure โ SHA-256 (libpq parity). RSA-PSS names its hash in the AlgorithmIdentifier parameters, not the OID.
proc md5AuthHash(user, password: string; salt: array[4, byte]): string {. ...raises: [], tags: [], forbids: [].}
- Compute MD5 authentication hash for PostgreSQL. Returns "md5" followed by hex of MD5(MD5(password+user) + salt).
proc scramClientFinalMessage(password: string; serverFirstData: openArray[byte]; state: var ScramState; maxIterations: int = DefaultMaxScramIterations): seq[ byte] {....raises: [PgConnectionError, PgConnectionError], tags: [RootEffect], forbids: [].}
- Generate the SCRAM-SHA-256 client-final message from the server's first response. Computes the client proof and stores the expected server signature in state.
proc scramClientFirstMessage(user: string; nonce: string; state: var ScramState; cbType: string = ""; cbData: seq[byte] = @[]; cbSupportedButUnused: bool = false): seq[byte] {. ...raises: [], tags: [], forbids: [].}
- Overload with explicit nonce for testing.
proc scramClientFirstMessage(user: string; state: var ScramState; cbType: string = ""; cbData: seq[byte] = @[]; cbSupportedButUnused: bool = false): seq[byte] {. ...raises: [PgConnectionError], tags: [], forbids: [].}
- Generate the SCRAM-SHA-256 client-first message with a random nonce. When cbType is non-empty, use channel binding (SCRAM-SHA-256-PLUS) and emit a "p=<type>,," gs2 header. When cbSupportedButUnused is set (TLS is in use but channel binding was not negotiated), emit "y,," so the server can detect a man-in-the-middle that stripped SCRAM-SHA-256-PLUS from the offered mechanisms. Otherwise emit "n,," (channel binding not supported).
proc scramEscapeUsername(user: string): string {....raises: [], tags: [], forbids: [].}
- Escape username for SCRAM per RFC 5802 Section 5.1. '=' is encoded as '=3D' and ',' is encoded as '=2C'.
proc scramVerifyServerFinal(serverFinalData: openArray[byte]; state: ScramState): bool {. ...raises: [], tags: [], forbids: [].}
- Verify the server's final SCRAM-SHA-256 signature matches the expected value. The caller is expected to wipe state.serverSignature after verification since it is no longer needed and would aid an attacker impersonating the server.