Skip to content

Commit a2decdd

Browse files
committed
Added Channel Binding supports #11
1 parent 873cc3a commit a2decdd

File tree

7 files changed

+191
-61
lines changed

7 files changed

+191
-61
lines changed

README.md

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,51 @@
11
[![Build Status](https://travis-ci.org/ba0f3/scram.nim.svg?branch=master)](https://travis-ci.org/ba0f3/scram.nim)
22

3-
# scram
3+
# scram.nim
44
Salted Challenge Response Authentication Mechanism (SCRAM)
55

66

7-
```nim
8-
var s = newScramClient[Sha256Digest]()
9-
s.clientNonce = "VeAOLsQ22fn/tjalHQIz7cQT"
7+
### Supported Mechanisms:
8+
* SCRAM-SHA-1
9+
* SCRAM-SHA-1-PLUS
10+
* SCRAM-SHA-256
11+
* SCRAM-SHA-256-PLUS
12+
* SCRAM-SHA-384
13+
* SCRAM-SHA-384-PLUS
14+
* SCRAM-SHA-512
15+
* SCRAM-SHA-512-PLUS
16+
* SCRAM-SHA3-512
17+
* SCRAM-SHA3-512-PLUS
18+
19+
### Supported Channel Binding Types
20+
* TLS_UNIQUE
21+
* TLS_SERVER_END_POINT
22+
23+
### Examples
1024

11-
echo s.prepareFirstMessage("bob")
12-
let finalMessage = s.prepareFinalMessage("secret", "r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,s=ldZSefTzKxPNJhP73AmW/A==,i=4096")
13-
echo finalMessage
14-
assert(finalMessage == "c=biws,r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,p=AtNtxGzsMA8evcWBM0MXFjxN8OcG1KRkLkFyoHlupOU=")
25+
#### Client
26+
```nim
27+
var client = newScramClient[Sha256Digest]()
28+
assert client.prepareFirstMessage(user) == cfirst, "incorrect first message"
29+
let fmsg = client.prepareFinalMessage(password, sfirst)
30+
assert fmsg == cfinal, "incorrect final message"
31+
assert client.verifyServerFinalMessage(sfinal), "incorrect server final message"
1532
```
33+
34+
#### Channel Binding
35+
36+
Helper proc `getChannelBindingData` added to helps you getting channel binding data from existing Socket/AsyncSocket
37+
38+
```nim
39+
var
40+
ctx = newContext()
41+
socket = newSocket()
42+
ctx.wrapSocket(socket)
43+
socket.connect(...)
44+
# ....
45+
let cbData = getChannelBindingData(TLS_UNIQUE, socket)
46+
47+
var client = newScramClient[Sha256Digest]()
48+
client.setChannelBindingType(TLS_UNIQUE)
49+
client.setChannelBindingData(cbData)
50+
echo client.prepareFirstMessage(user)
51+
```

scram.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version = "0.1.14"
1+
version = "0.2.0"
22
author = "Huy Doan"
33
description = "Salted Challenge Response Authentication Mechanism (SCRAM) "
44
license = "MIT"

scram/client.nim

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import base64, strformat, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]
22

33
export MD5Digest, SHA1Digest, SHA224Digest, SHA256Digest, SHA384Digest, SHA512Digest, Keccak512Digest
4-
export ChannelType
4+
export getChannelBindingData
55

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

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

22-
proc newScramClient*[T](socket: AnySocket, channel = TLS_UNIQUE): ScramClient[T] =
23-
result = newScramClient[T]()
24-
if socket != nil:
25-
validateCB(channel, socket)
26-
result.cbType = channel
27-
result.cbData = getCBData(channel, socket)
28-
29-
proc setCBindType*[T](s: ScramClient[T], channel: ChannelType) = s.cbType = channel
30-
31-
proc setCBindData*[T](s: ScramClient[T], data: string) = s.cbData = data
23+
proc setChannelBindingData*[T](s: ScramClient[T], data: string) = s.cbData = data
3224

3325
proc prepareFirstMessage*(s: ScramClient, username: string): string {.raises: [ScramError]} =
3426
if username.len == 0:

scram/private/types.nim

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
from net import Socket
2-
from asyncnet import AsyncSocket
3-
4-
export Socket, AsyncSocket
5-
61
type
72
ScramError* = object of CatchableError
83

@@ -19,8 +14,6 @@ type
1914
FIRST_CLIENT_MESSAGE_HANDLED
2015
ENDED
2116

22-
AnySocket* = Socket|AsyncSocket
23-
2417
ChannelType* = enum
2518
TLS_NONE = ""
2619
TLS_SERVER_END_POINT = "tls-server-end-point"

scram/private/utils.nim

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,37 @@ from sha1 import Sha1Digest
44
from nimSHA2 import Sha224Digest, Sha256Digest, Sha384Digest, Sha512Digest
55

66

7-
proc SSL_get_finished*(ssl: SslPtr, buf: cstring, count: csize_t): csize_t {.cdecl, dynlib: DLLSSLName, importc.}
8-
proc SSL_get_peer_finished*(ssl: SslPtr, buf: cstring, count: csize_t): csize_t {.cdecl, dynlib: DLLSSLName, importc.}
7+
#from net import Socket
8+
#from asyncnet import AsyncSocket
9+
10+
#export Socket, AsyncSocket
11+
12+
type
13+
AnySocket* = Socket|AsyncSocket
14+
15+
const
16+
NID_md5 = 4
17+
NID_md5_sha1 = 114
18+
EVP_MAX_MD_SIZE = 64
19+
20+
{.push cdecl, dynlib: DLLSSLName, importc.}
21+
22+
proc SSL_get_finished(ssl: SslPtr, buf: cstring, count: csize_t): csize_t
23+
proc SSL_get_peer_finished(ssl: SslPtr, buf: cstring, count: csize_t): csize_t
24+
25+
proc SSL_get_certificate(ssl: SslPtr): PX509
26+
proc SSL_get_peer_certificate(ssl: SslPtr): PX509
27+
28+
proc X509_get_signature_nid(x: PX509): int32
29+
proc OBJ_find_sigid_algs(signature: int32, pdigest: pointer, pencryption: pointer): int32
30+
proc OBJ_nid2sn(n: int): cstring
31+
32+
proc EVP_sha256(): PEVP_MD
33+
proc EVP_get_digestbynid(): PEVP_MD
34+
35+
proc X509_digest(data: PX509, kind: PEVP_MD, md: ptr char, len: ptr uint32): int32
36+
37+
{.pop.}
938

1039
randomize()
1140

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

106135

107-
proc validateCB*(channel: ChannelType, socket: AnySocket) =
136+
proc validateChannelBinding*(channel: ChannelType, socket: AnySocket) =
108137
if channel == TLS_NONE:
109138
return
110139

111140
if channel > TLS_EXPORT:
112-
raise newException(ScramChannelBindingError, "Channel type " & $channel & " is not supported")
141+
raise newException(ScramError, "Channel type " & $channel & " is not supported")
113142

114143
if socket.isNil:
115-
raise newException(ScramChannelBindingError, "Socket is not initialized")
144+
raise newException(ScramError, "Socket is not initialized")
116145

117146
if not socket.isSsl or socket.sslHandle() == nil:
118-
raise newException(ScramChannelBindingError, "Socket is not wrapped in a SSL context")
147+
raise newException(ScramError, "Socket is not wrapped in a SSL context")
148+
149+
proc getChannelBindingData*(channel: ChannelType, socket: AnySocket, isServer = true): string =
150+
# Ref: https://paquier.xyz/postgresql-2/channel-binding-openssl/
119151

120-
proc getCBData*(channel: ChannelType, socket: AnySocket, isServer = true): string =
152+
validateChannelBinding(channel, socket)
121153

122-
result = newString(1024)
154+
result = newString(EVP_MAX_MD_SIZE)
123155
if channel == TLS_UNIQUE:
124156
var ret: csize_t
125157
if isServer:
126-
ret = SSL_get_peer_finished(socket.sslHandle(), result.cstring, 1024)
158+
ret = SSL_get_peer_finished(socket.sslHandle(), result.cstring, EVP_MAX_MD_SIZE)
127159
else:
128-
ret = SSL_get_finished(socket.sslHandle(), result.cstring, 1024)
160+
ret = SSL_get_finished(socket.sslHandle(), result.cstring, EVP_MAX_MD_SIZE)
129161

130162
if ret == 0:
131-
raise newException(ScramChannelBindingError, "SSLError: handshake has not reached the finished message")
132-
163+
raise newException(ScramError, "SSLError: handshake has not reached the finished message")
133164
result.setLen(ret)
165+
166+
elif channel == TLS_SERVER_END_POINT:
167+
var
168+
serverCert: PX509
169+
algoNid: int32
170+
algoType: PEVP_MD
171+
hash: array[EVP_MAX_MD_SIZE, char]
172+
hashSize: int32
173+
174+
if isServer:
175+
serverCert = cast[PX509](SSL_get_certificate(socket.sslHandle()))
176+
else:
177+
serverCert = cast[PX509](SSL_get_peer_certificate(socket.sslHandle()))
178+
179+
if serverCert == nil:
180+
raise newException(ScramError, "SSLError: could not load server certtificate")
181+
182+
if OBJ_find_sigid_algs(X509_get_signature_nid(serverCert), addr algoNid, nil) == 0:
183+
raise newException(ScramError, "SSLError: could not determine server certificate signature algorithm")
184+
185+
if algoNid == NID_md5 or algoNid == NID_md5_sha1:
186+
algoType = EVP_sha256()
187+
else:
188+
algoType = EVP_get_digestbynid(algoNid)
189+
if algoType == nil:
190+
raise newException(ScramError, "SSLError: could not find digest for NID " & OBJ_nid2sn(algoNid))
191+
192+
if X509_digest(serverCert, algoType, hash, addr hashSize) == 0:
193+
raise newException(ScramError, "SSLError: could not generate server certificate hash")
194+
195+
copyMem(addr result[0], hash, hashSize)
196+
result.setLen(hashSize)
197+
198+
else:
199+
raise newException(ScramError, "Channel " & $channel & " is not supported yet")

scram/server.nim

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import strformat, strutils, base64, hmac, nimSHA2, private/[utils,types]
1+
import base64, strformat, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]
22

3-
export ChannelType
3+
export MD5Digest, SHA1Digest, SHA224Digest, SHA256Digest, SHA384Digest, SHA512Digest, Keccak512Digest
4+
export getChannelBindingData
45

56
type
67
ScramServer[T] = ref object of RootObj
@@ -52,16 +53,9 @@ proc newScramServer*[T](): ScramServer[T] =
5253
result.isSuccessful = false
5354
result.cbType = TLS_NONE
5455

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

58-
result = newScramServer[T]()
59-
result.cbType = channel
60-
result.cbData = getCBData(channel, socket)
61-
62-
proc setCBindType*[T](s: ScramServer[T], channel: ChannelType) = s.cbType = channel
63-
64-
proc setCBindData*[T](s: ScramServer[T], data: string) = s.cbData = data
58+
proc setChannelBindingData*[T](s: ScramServer[T], data: string) = s.cbData = data
6559

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

tests/test_cb.nim

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ import scram/private/types
33

44
const FAKE_CBDATA = "xxxxxxxxxxxxxxxx"
55

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

10-
client.setCBindType(TLS_UNIQUE)
11-
client.setCBindData(FAKE_CBDATA)
10+
if clientChannel != TLS_NONE:
11+
client.setChannelBindingType(clientChannel)
12+
client.setChannelBindingData(clientCbData)
1213

13-
server.setCBindType(TLS_UNIQUE)
14-
server.setCBindData(FAKE_CBDATA)
14+
if serverChannel != TLS_NONE:
15+
server.setChannelBindingType(serverChannel)
16+
server.setChannelBindingData(serverCbData)
1517

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

24-
suite "Scram Client-Server tests":
25-
test "SCRAM-SHA1-PLUS":
26+
suite "Scram Channel Binding tests":
27+
test "SCRAM-SHA1-PLUS tls-unique":
2628
test[Sha1Digest](
2729
"user",
28-
"pencil"
30+
"pencil",
31+
TLS_UNIQUE,
32+
TLS_UNIQUE
2933
)
3034

31-
test "SCRAM-SHA256-PLUS":
35+
test "SCRAM-SHA256-PLUS: tls-unique":
3236
test[Sha256Digest](
3337
"bob",
34-
"secret"
35-
)
38+
"secret",
39+
TLS_UNIQUE,
40+
TLS_UNIQUE
41+
)
42+
43+
test "SCRAM-SHA1-PLUS tls-server-end-point":
44+
test[Sha1Digest](
45+
"user",
46+
"pencil",
47+
TLS_SERVER_END_POINT,
48+
TLS_SERVER_END_POINT
49+
)
50+
51+
test "SCRAM-SHA256-PLUS: tls-server-end-point":
52+
test[Sha256Digest](
53+
"bob",
54+
"secret",
55+
TLS_SERVER_END_POINT,
56+
TLS_SERVER_END_POINT
57+
)
58+
59+
test "client-support-server-do-not":
60+
expect ScramError:
61+
test[Sha256Digest](
62+
"bob",
63+
"secret",
64+
TLS_UNIQUE
65+
)
66+
67+
test "server-do-not-suport-client-channel-binding-type":
68+
expect ScramError:
69+
test[Sha256Digest](
70+
"bob",
71+
"secret",
72+
TLS_UNIQUE,
73+
TLS_SERVER_END_POINT
74+
)
75+
test "channel-bindings-dont-match":
76+
expect ScramError:
77+
test[Sha256Digest](
78+
"bob",
79+
"secret",
80+
TLS_UNIQUE,
81+
TLS_UNIQUE,
82+
"xxxx",
83+
"zzzz"
84+
)

0 commit comments

Comments
 (0)