botan-low
Copyright(c) 2023-2024 Apotheca Labs
(c) 2024-2025 Haskell Foundation
LicenseBSD-3-Clause
Maintainerjoris@well-typed.com, leo@apotheca.io
Stabilityexperimental
PortabilityPOSIX
Safe HaskellSafe-Inferred
LanguageGHC2021

Botan.Low.SRP6

Description

This module is based on the Secure Remote Password section of the C++ API reference.

Synopsis

Example usage

This is a simplified, executable example that should give you an idea of how to use the functions in this module to run the SRP6 protocol. We will not explain the protocol in detail here, so consult other sources if you need details about the algorithm. The simplified example shows how a client and server run the protocol from separate threads, keeping private values secret and communicating public values using message passing. In practice, the client and server will probably run on separate machines (or processes) with secure communication channels in place, and other security measures are necessary. So, don't use this example directly in your code if it runs in production.

Let us start with some necessary imports.

>>> :{
{-# OPTIONS_GHC -threaded -Wall #-}
{-# LANGUAGE OverloadedStrings #-}
import           Botan.Low.Hash
import           Botan.Low.PubKey
import           Botan.Low.RNG
import           Botan.Low.SRP6
import           Control.Concurrent
import           Control.Concurrent.Async
import           Data.ByteString hiding (group)
import           Data.IORef
import           Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import           Data.Maybe
import           Text.Printf
:}

The client and server exchange a number of messages during the protocol. They will be explained in more detail later as they become relevant for the part of the protocol that we are discussing. The messages will be exchanged from the client to the server using an MVar ClientMsg, and from the server to the client using an MVar ServerMsg.

>>> :{
type Identifier = ByteString
data ClientMsg =
    Signup Identifier SRP6Verifier
  | StartAuthentication Identifier
  | SendClientPublicKey SRP6AValue
data ServerMsg =
    SendServerPublicKey SRP6BValue
:}

SRP works in a discrete logarithm group, and it computes hashes using a hash family. Both can be configured, and we will configure them as follows:

>>> :{
group :: DLGroupName
group = MODP_SRP_4096
hash :: HashName
hash = SHA512
:}

When a client wants to sign up with the server with a combination of an identifier and password, the client first generates a verifier using the identifier, the password, and a locally generated salt. Then, the client sends their identifier and verifier to the server, while it stores its own salt locally:

>>> :{
type Password = ByteString
type Salt = ByteString
clientSignup :: MVar ClientMsg -> Identifier -> Password -> IO Salt
clientSignup clientMsgVar identifier password = do
    rng <- rngInit UserRNG
    salt <- rngGet rng 12
    verifier <- srp6GenerateVerifier identifier password salt group hash
    putMVar clientMsgVar $ Signup identifier verifier
    pure salt
:}

The server receives the signup request, and stores the identifier with the verifier:

>>> :{
type Users = Map Identifier SRP6Verifier
serverSignup :: MVar ClientMsg -> IORef Users -> IO ()
serverSignup clientMsgVar usersRef = do
    Signup identifier verifier <- takeMVar clientMsgVar
    modifyIORef usersRef $ Map.insert identifier verifier
:}

Later, the client may request to authenticate itself to the server:

>>> :{
clientStartAuthentication :: MVar ClientMsg -> Identifier -> IO ()
clientStartAuthentication clientMsgVar identifier = do
    putMVar clientMsgVar $ StartAuthentication identifier
:}

The server receives the authentication request, looks up the verfier, generates the server's public key (a SRP6 B value), and sends it back to the client:

>>> :{
serverStartAuthentication :: MVar ClientMsg -> MVar ServerMsg -> IORef Users -> IO SRP6ServerSession
serverStartAuthentication clientMsgVar serverMsg usersRef = do
    StartAuthentication identifier <- takeMVar clientMsgVar
    users <- readIORef usersRef
    let verifier = fromJust $ Map.lookup identifier users
    rng <- rngInit UserRNG
    session <- srp6ServerSessionInit
    serverPublicKey <- srp6ServerSessionStep1 session verifier group hash rng
    putMVar serverMsg $ SendServerPublicKey serverPublicKey
    pure session
:}

Once the client receives the server's public key, it generates the client's public key (an SRP6 B value) and the shared secret key, and sends the client's public key to the server:

>>> :{
clientAuthentication :: MVar ClientMsg -> MVar ServerMsg -> Identifier -> Password -> Salt -> IO SRP6SharedSecret
clientAuthentication clientMsgVar serverMsgVar identifier password salt = do
    SendServerPublicKey serverPublicKey <- takeMVar serverMsgVar
    rng <- rngInit UserRNG
    (clientPublicKey, clientSecretKey) <-
      srp6ClientAgree identifier password group hash salt serverPublicKey rng
    putMVar clientMsgVar $ SendClientPublicKey clientPublicKey
    pure clientSecretKey
:}

The server then receives the client's public key, and generates a matching shared secret key:

>>> :{
serverAuthentication :: MVar ClientMsg -> SRP6ServerSession -> IO SRP6SharedSecret
serverAuthentication clientMsgVar session = do
    SendClientPublicKey clientPublicKey <- takeMVar clientMsgVar
    serverSecretKey <- srp6ServerSessionStep2 session group clientPublicKey
    pure serverSecretKey
:}

At this point, the client's and servers' secret key should be equal. In practice, the secret keys should be confirmed to match by exchanging a hash digest of relevant protocol values, susch as the secret key, public keys, identifier, salt, and more. Calculation of this hash digest is not part of this example. Instead, we will return the secret keys from the client and server so that we can compare them, which obviously breaks security, but this should be fine for our example.

We bring the client-side of the algorithm together by picking an identifier and password, and calling the client-side functions in order.

>>> :{
client :: MVar ClientMsg -> MVar ServerMsg -> IO SRP6SharedSecret
client clientMsgVar serverMsgVar = do
    let identifier = "alice"
        password = "Fee fi fo fum!"
    salt <- clientSignup clientMsgVar identifier password
    clientStartAuthentication clientMsgVar identifier
    clientAuthentication clientMsgVar serverMsgVar identifier password salt
:}

We bring the server-side of the algorithm together by initialising a session and calling the server-side functions in order.

>>> :{
server :: MVar ClientMsg -> MVar ServerMsg -> IO SRP6SharedSecret
server clientMsgVar serverMsgVar = do
    usersRef <- newIORef Map.empty
    serverSignup clientMsgVar usersRef
    session <- serverStartAuthentication clientMsgVar serverMsgVar usersRef
    serverAuthentication clientMsgVar session
:}

Using the async package, we spin up a thread for the client and a thread for the server. We wait for them to finish, and then compare the resulting secret keys.

>>> :{
main :: IO ()
main = do
  clientMsgVar <- newEmptyMVar
  serverMsgVar <- newEmptyMVar
  (clientSecretKey, serverSecretKey) <-
    concurrently (client clientMsgVar serverMsgVar) (server clientMsgVar serverMsgVar)
  let match = clientSecretKey == serverSecretKey
  if match then
    print @String "client secret == server secret"
  else
    error $
      printf "%s /= %s"
              (show clientSecretKey)
              (show serverSecretKey)
:}

All that is left to do is run the program.

>>> main
"client secret == server secret"

Password hashing

srp6ServerSessionInit Source #

Arguments

:: IO SRP6ServerSession

srp6: SRP-6 server session object

Initialize an SRP-6 server session object

srp6ServerSessionDestroy :: SRP6ServerSession -> IO () Source #

Frees all resources of the SRP-6 server session object

srp6ServerSessionStep1 Source #

Arguments

:: SRP6ServerSession

srp6: SRP-6 server session object

-> SRP6Verifier

verifier[]: the verification value saved from client registration

-> DLGroupName

group_id: the SRP group id

-> HashName

hash_id: the SRP hash in use

-> RNG

rng_obj: a random number generator object

-> IO SRP6BValue

B_pub[]: out buffer to store the SRP-6 B value

SRP-6 Server side step 1

NOTE: this function should be not be invoked twice on the same server session. Regardless of the result of the first invocation, the second invocation will result in an error. See https://github.com/randombit/botan/issues/5112 for more information. If a second invocation can not be prevented, try it on a newly initialised server session instead.

srp6ServerSessionStep2 Source #

Arguments

:: SRP6ServerSession

srp6: SRP-6 server session object

-> SRP6AValue

A[]: the client's value

-> DLGroupName

the SRP group id

-> IO SRP6SharedSecret

key[]: out buffer to store the symmetric key value

SRP-6 Server side step 2

srp6GenerateVerifier Source #

Arguments

:: Identifier

identifier: a username or other client identifier

-> Password

password: the secret used to authenticate user

-> Salt

salt[]: a randomly chosen value, at least 128 bits long

-> DLGroupName

group_id: specifies the shared SRP group

-> HashName

hash_id: specifies a secure hash function

-> IO SRP6Verifier

verifier[]: out buffer to store the SRP-6 verifier value

Generate a new SRP-6 verifier

srp6ClientAgree Source #

Arguments

:: Identifier

username: the username we are attempting login for

-> Password

password: the password we are attempting to use

-> DLGroupName

group_id: specifies the shared SRP group

-> HashName

hash_id: specifies a secure hash function

-> Salt

salt[]: is the salt value sent by the server

-> SRP6BValue

B[]: is the server's public value

-> RNG

rng_obj: is a random number generator object

-> IO (SRP6AValue, SRP6SharedSecret)

A tuple of two elements (in order):

  • A[]: out buffer to store the SRP-6 A value
  • K[]: out buffer to store the symmetric value

SRP6a Client side

srp6GroupSize Source #

Arguments

:: DLGroupName

group_id

-> IO Int

group_p_bytes

Return the size, in bytes, of the prime associated with group_id

This function can be used to determine the size of output buffers for generated keys in the SRP6 algorithm. Such buffers need to be allocated before calling SRP6 functions. An example of such a buffer is the verifier[] buffer in the srp6GenerateVerifier function. In botan-low, such buffers are created internally using srp6GroupSize, without requiring input from you (the user).

SRP6 Types

SRP discrete logarithm groups