Skip to content

Commit

Permalink
Merge pull request #21 from ba0f3/channel-binding
Browse files Browse the repository at this point in the history
Add Channel binding supports
  • Loading branch information
ba0f3 authored Aug 2, 2022
2 parents 499f680 + a2decdd commit 105d094
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 85 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
70 changes: 29 additions & 41 deletions scram/client.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import strformat
import base64, pegs, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]
import base64, strformat, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]

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

type
ScramClient[T] = ref object of RootObj
Expand All @@ -10,29 +10,17 @@ type
state: ScramState
isSuccessful: bool
serverSignature: T

when compileOption("threads"):
var
SERVER_FIRST_MESSAGE_VAL: ptr Peg
SERVER_FINAL_MESSAGE_VAL: ptr Peg
template SERVER_FIRST_MESSAGE: Peg =
if SERVER_FIRST_MESSAGE_VAL.isNil:
SERVER_FIRST_MESSAGE_VAL = cast[ptr Peg](allocShared0(sizeof(Peg)))
SERVER_FIRST_MESSAGE_VAL[] = peg"'r='{[^,]*}',s='{[^,]*}',i='{\d+}$"
SERVER_FIRST_MESSAGE_VAL[]
template SERVER_FINAL_MESSAGE: Peg =
if SERVER_FINAL_MESSAGE_VAL.isNil:
SERVER_FINAL_MESSAGE_VAL = cast[ptr Peg](allocShared0(sizeof(Peg)))
SERVER_FINAL_MESSAGE_VAL[] = peg"'v='{[^,]*}$"
SERVER_FINAL_MESSAGE_VAL[]
else:
let
SERVER_FIRST_MESSAGE = peg"'r='{[^,]*}',s='{[^,]*}',i='{\d+}$"
SERVER_FINAL_MESSAGE = peg"'v='{[^,]*}$"
cbType: ChannelType
cbData: string

proc newScramClient*[T](): ScramClient[T] =
result = new(ScramClient[T])
result = new ScramClient[T]
result.clientNonce = makeNonce()
result.cbType = TLS_NONE

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

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 All @@ -44,7 +32,7 @@ proc prepareFirstMessage*(s: ScramClient, username: string): string {.raises: [S
s.clientFirstMessageBare.add(s.clientNonce)

s.state = FIRST_PREPARED
GS2_HEADER & s.clientFirstMessageBare
result = makeGS2Header(s.cbType) & s.clientFirstMessageBare

proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: string): string =
if s.state != FIRST_PREPARED:
Expand All @@ -53,17 +41,15 @@ proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: st
nonce, salt: string
iterations: int
var matches: array[3, string]
if match(serverFirstMessage, SERVER_FIRST_MESSAGE, matches):
for kv in serverFirstMessage.split(','):
if kv[0..1] == "i=":
iterations = parseInt(kv[2..^1])
elif kv[0..1] == "r=":
nonce = kv[2..^1]
elif kv[0..1] == "s=":
salt = base64.decode(kv[2..^1])
else:
s.state = ENDED
return ""

for kv in serverFirstMessage.split(','):
if kv[0..1] == "i=":
iterations = parseInt(kv[2..^1])
elif kv[0..1] == "r=":
nonce = kv[2..^1]
elif kv[0..1] == "s=":
salt = base64.decode(kv[2..^1])


if not nonce.startsWith(s.clientNonce):
raise newException(ScramError, "Security error: invalid nonce received from server. Possible man-in-the-middle attack.")
Expand All @@ -76,7 +62,7 @@ proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: st
clientKey = HMAC[T]($%saltedPassword, CLIENT_KEY)
storedKey = HASH[T]($%clientKey)
serverKey = HMAC[T]($%saltedPassword, SERVER_KEY)
clientFinalMessageWithoutProof = "c=biws,r=" & nonce
clientFinalMessageWithoutProof = makeCBind(s.cbType, s.cbData) & ",r=" & nonce
authMessage =[s.clientFirstMessageBare, serverFirstMessage, clientFinalMessageWithoutProof].join(",")
clientSignature = HMAC[T]($%storedKey, authMessage)
s.serverSignature = HMAC[T]($%serverKey, authMessage)
Expand All @@ -93,12 +79,14 @@ proc verifyServerFinalMessage*(s: ScramClient, serverFinalMessage: string): bool
raise newException(ScramError, "You can call this method only once after calling prepareFinalMessage()")
s.state = ENDED
var matches: array[1, string]
if match(serverFinalMessage, SERVER_FINAL_MESSAGE, matches):
var proposedServerSignature: string
for kv in serverFinalMessage.split(','):
if kv[0..1] == "v=":
proposedServerSignature = base64.decode(kv[2..^1])
s.isSuccessful = proposedServerSignature == $%s.serverSignature

var proposedServerSignature: string
for kv in serverFinalMessage.split(','):
if kv[0..1] == "e=":
raise newException(ScramError, "ServerError: " & kv[2..^1])
elif kv[0..1] == "v=":
proposedServerSignature = base64.decode(kv[2..^1])
s.isSuccessful = proposedServerSignature == $%s.serverSignature
s.isSuccessful

proc isSuccessful*(s: ScramClient): bool =
Expand Down
23 changes: 22 additions & 1 deletion scram/private/types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,29 @@ type
FIRST_CLIENT_MESSAGE_HANDLED
ENDED

ChannelType* = enum
TLS_NONE = ""
TLS_SERVER_END_POINT = "tls-server-end-point"
TLS_UNIQUE = "tls-unique"
TLS_UNIQUE_FOR_TELNET = "tls-server-for-telnet"
TLS_EXPORT = "tls-export"

ServerError* = enum
SERVER_ERROR_NO_ERROR = ""
SERVER_ERROR_INVALID_ENCODING = "invalid-encoding"
SERVER_ERROR_EXTENSIONS_NOT_SUPPORTED = "extensions-not-supported"
SERVER_ERROR_INVALID_PROOF = "invalid-proof"
SERVER_ERROR_CHANNEL_BINDINGS_DONT_MATCH = "channel-bindings-dont-match"
SERVER_ERROR_SERVER_DOES_SUPPORT_CHANNEL_BINDING = "server-does-support-channel-binding"
SERVER_ERROR_CHANNEL_BINDING_NOT_SUPPORTED = "channel-binding-not-supported"
SERVER_ERROR_UNSUPPORTED_CHANNEL_BINDING_TYPE = "unsupported-channel-binding-type"
SERVER_ERROR_UNKNOWN_USER = "unknown-user"
SERVER_ERROR_INVALID_USERNAME_ENCODING = "invalid-username-encoding"
SERVER_ERROR_NO_RESOURCES = "no-resources"
SERVER_ERROR_OTHER_ERROR = "other-error"


const
GS2_HEADER* = "n,,"
INT_1* = "\x00\x00\x00\x01"
CLIENT_KEY* = "Client Key"
SERVER_KEY* = "Server Key"
117 changes: 115 additions & 2 deletions scram/private/utils.nim
Original file line number Diff line number Diff line change
@@ -1,8 +1,41 @@
import random, base64, strutils, types, hmac, bitops
import random, base64, strutils, types, hmac, bitops, openssl, net, asyncnet
from md5 import MD5Digest
from sha1 import Sha1Digest
from nimSHA2 import Sha224Digest, Sha256Digest, Sha384Digest, Sha512Digest


#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()

proc `$%`*[T](input: T): string =
Expand Down Expand Up @@ -83,4 +116,84 @@ proc hi*[T](password, salt: string, iterations: int): T =
result = previous
for _ in 1..<iterations:
previous = HMAC[T](password, $%previous)
result ^= previous
result ^= previous

proc makeGS2Header*(channel: ChannelType): string =
result = case channel
of TLS_UNIQUE: "p=tls-unique,,"
of TLS_SERVER_END_POINT: "p=tls-server-end-point,,"
of TLS_UNIQUE_FOR_TELNET: "p=tls-server-for-telnet,,"
of TLS_EXPORT: "p=tls-export,,"
else: "n,,"

proc makeCBind*(channel: ChannelType, data: string = ""): string =
if channel == TLS_NONE:
result = "c=biws"
else:
result = "c=" & base64.encode(makeGS2Header(channel) & data)


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

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

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

if not socket.isSsl or socket.sslHandle() == nil:
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/

validateChannelBinding(channel, socket)

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, EVP_MAX_MD_SIZE)
else:
ret = SSL_get_finished(socket.sslHandle(), result.cstring, EVP_MAX_MD_SIZE)

if ret == 0:
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")
Loading

0 comments on commit 105d094

Please sign in to comment.