Skip to content

Commit

Permalink
Added Channel Binding supports #11
Browse files Browse the repository at this point in the history
  • Loading branch information
ba0f3 committed Aug 2, 2022
1 parent 873cc3a commit a2decdd
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 61 deletions.
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
[![Build Status](https://travis-ci.org/ba0f3/scram.nim.svg?branch=master)](https://travis-ci.org/ba0f3/scram.nim)

# scram
# scram.nim
Salted Challenge Response Authentication Mechanism (SCRAM)


```nim
var s = newScramClient[Sha256Digest]()
s.clientNonce = "VeAOLsQ22fn/tjalHQIz7cQT"
### Supported Mechanisms:
* SCRAM-SHA-1
* SCRAM-SHA-1-PLUS
* SCRAM-SHA-256
* SCRAM-SHA-256-PLUS
* SCRAM-SHA-384
* SCRAM-SHA-384-PLUS
* SCRAM-SHA-512
* SCRAM-SHA-512-PLUS
* SCRAM-SHA3-512
* SCRAM-SHA3-512-PLUS

### Supported Channel Binding Types
* TLS_UNIQUE
* TLS_SERVER_END_POINT

### Examples

echo s.prepareFirstMessage("bob")
let finalMessage = s.prepareFinalMessage("secret", "r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,s=ldZSefTzKxPNJhP73AmW/A==,i=4096")
echo finalMessage
assert(finalMessage == "c=biws,r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,p=AtNtxGzsMA8evcWBM0MXFjxN8OcG1KRkLkFyoHlupOU=")
#### Client
```nim
var client = newScramClient[Sha256Digest]()
assert client.prepareFirstMessage(user) == cfirst, "incorrect first message"
let fmsg = client.prepareFinalMessage(password, sfirst)
assert fmsg == cfinal, "incorrect final message"
assert client.verifyServerFinalMessage(sfinal), "incorrect server final message"
```

#### Channel Binding

Helper proc `getChannelBindingData` added to helps you getting channel binding data from existing Socket/AsyncSocket

```nim
var
ctx = newContext()
socket = newSocket()
ctx.wrapSocket(socket)
socket.connect(...)
# ....
let cbData = getChannelBindingData(TLS_UNIQUE, socket)
var client = newScramClient[Sha256Digest]()
client.setChannelBindingType(TLS_UNIQUE)
client.setChannelBindingData(cbData)
echo client.prepareFirstMessage(user)
```
2 changes: 1 addition & 1 deletion scram.nimble
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = "0.1.14"
version = "0.2.0"
author = "Huy Doan"
description = "Salted Challenge Response Authentication Mechanism (SCRAM) "
license = "MIT"
Expand Down
14 changes: 3 additions & 11 deletions scram/client.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import base64, strformat, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]

export MD5Digest, SHA1Digest, SHA224Digest, SHA256Digest, SHA384Digest, SHA512Digest, Keccak512Digest
export ChannelType
export getChannelBindingData

type
ScramClient[T] = ref object of RootObj
Expand All @@ -18,17 +18,9 @@ proc newScramClient*[T](): ScramClient[T] =
result.clientNonce = makeNonce()
result.cbType = TLS_NONE

proc setChannelBindingType*[T](s: ScramClient[T], channel: ChannelType) = s.cbType = channel

proc newScramClient*[T](socket: AnySocket, channel = TLS_UNIQUE): ScramClient[T] =
result = newScramClient[T]()
if socket != nil:
validateCB(channel, socket)
result.cbType = channel
result.cbData = getCBData(channel, socket)

proc setCBindType*[T](s: ScramClient[T], channel: ChannelType) = s.cbType = channel

proc setCBindData*[T](s: ScramClient[T], data: string) = s.cbData = data
proc setChannelBindingData*[T](s: ScramClient[T], data: string) = s.cbData = data

proc prepareFirstMessage*(s: ScramClient, username: string): string {.raises: [ScramError]} =
if username.len == 0:
Expand Down
7 changes: 0 additions & 7 deletions scram/private/types.nim
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
from net import Socket
from asyncnet import AsyncSocket

export Socket, AsyncSocket

type
ScramError* = object of CatchableError

Expand All @@ -19,8 +14,6 @@ type
FIRST_CLIENT_MESSAGE_HANDLED
ENDED

AnySocket* = Socket|AsyncSocket

ChannelType* = enum
TLS_NONE = ""
TLS_SERVER_END_POINT = "tls-server-end-point"
Expand Down
90 changes: 78 additions & 12 deletions scram/private/utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,37 @@ from sha1 import Sha1Digest
from nimSHA2 import Sha224Digest, Sha256Digest, Sha384Digest, Sha512Digest


proc SSL_get_finished*(ssl: SslPtr, buf: cstring, count: csize_t): csize_t {.cdecl, dynlib: DLLSSLName, importc.}
proc SSL_get_peer_finished*(ssl: SslPtr, buf: cstring, count: csize_t): csize_t {.cdecl, dynlib: DLLSSLName, importc.}
#from net import Socket
#from asyncnet import AsyncSocket

#export Socket, AsyncSocket

type
AnySocket* = Socket|AsyncSocket

const
NID_md5 = 4
NID_md5_sha1 = 114
EVP_MAX_MD_SIZE = 64

{.push cdecl, dynlib: DLLSSLName, importc.}

proc SSL_get_finished(ssl: SslPtr, buf: cstring, count: csize_t): csize_t
proc SSL_get_peer_finished(ssl: SslPtr, buf: cstring, count: csize_t): csize_t

proc SSL_get_certificate(ssl: SslPtr): PX509
proc SSL_get_peer_certificate(ssl: SslPtr): PX509

proc X509_get_signature_nid(x: PX509): int32
proc OBJ_find_sigid_algs(signature: int32, pdigest: pointer, pencryption: pointer): int32
proc OBJ_nid2sn(n: int): cstring

proc EVP_sha256(): PEVP_MD
proc EVP_get_digestbynid(): PEVP_MD

proc X509_digest(data: PX509, kind: PEVP_MD, md: ptr char, len: ptr uint32): int32

{.pop.}

randomize()

Expand Down Expand Up @@ -104,30 +133,67 @@ proc makeCBind*(channel: ChannelType, data: string = ""): string =
result = "c=" & base64.encode(makeGS2Header(channel) & data)


proc validateCB*(channel: ChannelType, socket: AnySocket) =
proc validateChannelBinding*(channel: ChannelType, socket: AnySocket) =
if channel == TLS_NONE:
return

if channel > TLS_EXPORT:
raise newException(ScramChannelBindingError, "Channel type " & $channel & " is not supported")
raise newException(ScramError, "Channel type " & $channel & " is not supported")

if socket.isNil:
raise newException(ScramChannelBindingError, "Socket is not initialized")
raise newException(ScramError, "Socket is not initialized")

if not socket.isSsl or socket.sslHandle() == nil:
raise newException(ScramChannelBindingError, "Socket is not wrapped in a SSL context")
raise newException(ScramError, "Socket is not wrapped in a SSL context")

proc getChannelBindingData*(channel: ChannelType, socket: AnySocket, isServer = true): string =
# Ref: https://paquier.xyz/postgresql-2/channel-binding-openssl/

proc getCBData*(channel: ChannelType, socket: AnySocket, isServer = true): string =
validateChannelBinding(channel, socket)

result = newString(1024)
result = newString(EVP_MAX_MD_SIZE)
if channel == TLS_UNIQUE:
var ret: csize_t
if isServer:
ret = SSL_get_peer_finished(socket.sslHandle(), result.cstring, 1024)
ret = SSL_get_peer_finished(socket.sslHandle(), result.cstring, EVP_MAX_MD_SIZE)
else:
ret = SSL_get_finished(socket.sslHandle(), result.cstring, 1024)
ret = SSL_get_finished(socket.sslHandle(), result.cstring, EVP_MAX_MD_SIZE)

if ret == 0:
raise newException(ScramChannelBindingError, "SSLError: handshake has not reached the finished message")

raise newException(ScramError, "SSLError: handshake has not reached the finished message")
result.setLen(ret)

elif channel == TLS_SERVER_END_POINT:
var
serverCert: PX509
algoNid: int32
algoType: PEVP_MD
hash: array[EVP_MAX_MD_SIZE, char]
hashSize: int32

if isServer:
serverCert = cast[PX509](SSL_get_certificate(socket.sslHandle()))
else:
serverCert = cast[PX509](SSL_get_peer_certificate(socket.sslHandle()))

if serverCert == nil:
raise newException(ScramError, "SSLError: could not load server certtificate")

if OBJ_find_sigid_algs(X509_get_signature_nid(serverCert), addr algoNid, nil) == 0:
raise newException(ScramError, "SSLError: could not determine server certificate signature algorithm")

if algoNid == NID_md5 or algoNid == NID_md5_sha1:
algoType = EVP_sha256()
else:
algoType = EVP_get_digestbynid(algoNid)
if algoType == nil:
raise newException(ScramError, "SSLError: could not find digest for NID " & OBJ_nid2sn(algoNid))

if X509_digest(serverCert, algoType, hash, addr hashSize) == 0:
raise newException(ScramError, "SSLError: could not generate server certificate hash")

copyMem(addr result[0], hash, hashSize)
result.setLen(hashSize)

else:
raise newException(ScramError, "Channel " & $channel & " is not supported yet")
16 changes: 5 additions & 11 deletions scram/server.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import strformat, strutils, base64, hmac, nimSHA2, private/[utils,types]
import base64, strformat, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]

export ChannelType
export MD5Digest, SHA1Digest, SHA224Digest, SHA256Digest, SHA384Digest, SHA512Digest, Keccak512Digest
export getChannelBindingData

type
ScramServer[T] = ref object of RootObj
Expand Down Expand Up @@ -52,16 +53,9 @@ proc newScramServer*[T](): ScramServer[T] =
result.isSuccessful = false
result.cbType = TLS_NONE

proc newScramServer*[T](socket: AnySocket, channel = TLS_UNIQUE): ScramServer[T] =
validateCB(channel, socket)
proc setChannelBindingType*[T](s: ScramServer[T], channel: ChannelType) = s.cbType = channel

result = newScramServer[T]()
result.cbType = channel
result.cbData = getCBData(channel, socket)

proc setCBindType*[T](s: ScramServer[T], channel: ChannelType) = s.cbType = channel

proc setCBindData*[T](s: ScramServer[T], data: string) = s.cbData = data
proc setChannelBindingData*[T](s: ScramServer[T], data: string) = s.cbData = data

proc setServerNonce*[T](s: ScramServer[T], nonce: string) = s.serverNonce = nonce

Expand Down
71 changes: 60 additions & 11 deletions tests/test_cb.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import scram/private/types

const FAKE_CBDATA = "xxxxxxxxxxxxxxxx"

proc test[T](user, password: string) =
proc test[T](user, password: string, clientChannel = TLS_NONE, serverChannel = TLS_NONE, clientCbData = FAKE_CBDATA, serverCbData = FAKE_CBDATA) =
var client = newScramClient[T]()
var server = newScramServer[T]()

client.setCBindType(TLS_UNIQUE)
client.setCBindData(FAKE_CBDATA)
if clientChannel != TLS_NONE:
client.setChannelBindingType(clientChannel)
client.setChannelBindingData(clientCbData)

server.setCBindType(TLS_UNIQUE)
server.setCBindData(FAKE_CBDATA)
if serverChannel != TLS_NONE:
server.setChannelBindingType(serverChannel)
server.setChannelBindingData(serverCbData)

let cfirst = client.prepareFirstMessage(user)
assert server.handleClientFirstMessage(cfirst) == user, "incorrect detected username"
Expand All @@ -21,15 +23,62 @@ proc test[T](user, password: string) =
let sfinal = server.prepareFinalMessage(cfinal)
assert client.verifyServerFinalMessage(sfinal), "incorrect server final message"

suite "Scram Client-Server tests":
test "SCRAM-SHA1-PLUS":
suite "Scram Channel Binding tests":
test "SCRAM-SHA1-PLUS tls-unique":
test[Sha1Digest](
"user",
"pencil"
"pencil",
TLS_UNIQUE,
TLS_UNIQUE
)

test "SCRAM-SHA256-PLUS":
test "SCRAM-SHA256-PLUS: tls-unique":
test[Sha256Digest](
"bob",
"secret"
)
"secret",
TLS_UNIQUE,
TLS_UNIQUE
)

test "SCRAM-SHA1-PLUS tls-server-end-point":
test[Sha1Digest](
"user",
"pencil",
TLS_SERVER_END_POINT,
TLS_SERVER_END_POINT
)

test "SCRAM-SHA256-PLUS: tls-server-end-point":
test[Sha256Digest](
"bob",
"secret",
TLS_SERVER_END_POINT,
TLS_SERVER_END_POINT
)

test "client-support-server-do-not":
expect ScramError:
test[Sha256Digest](
"bob",
"secret",
TLS_UNIQUE
)

test "server-do-not-suport-client-channel-binding-type":
expect ScramError:
test[Sha256Digest](
"bob",
"secret",
TLS_UNIQUE,
TLS_SERVER_END_POINT
)
test "channel-bindings-dont-match":
expect ScramError:
test[Sha256Digest](
"bob",
"secret",
TLS_UNIQUE,
TLS_UNIQUE,
"xxxx",
"zzzz"
)

0 comments on commit a2decdd

Please sign in to comment.