Skip to content

Commit 105d094

Browse files
authored
Merge pull request #21 from ba0f3/channel-binding
Add Channel binding supports
2 parents 499f680 + a2decdd commit 105d094

File tree

10 files changed

+375
-85
lines changed

10 files changed

+375
-85
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: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import strformat
2-
import base64, pegs, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]
1+
import base64, strformat, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]
32

43
export MD5Digest, SHA1Digest, SHA224Digest, SHA256Digest, SHA384Digest, SHA512Digest, Keccak512Digest
4+
export getChannelBindingData
55

66
type
77
ScramClient[T] = ref object of RootObj
@@ -10,29 +10,17 @@ type
1010
state: ScramState
1111
isSuccessful: bool
1212
serverSignature: T
13-
14-
when compileOption("threads"):
15-
var
16-
SERVER_FIRST_MESSAGE_VAL: ptr Peg
17-
SERVER_FINAL_MESSAGE_VAL: ptr Peg
18-
template SERVER_FIRST_MESSAGE: Peg =
19-
if SERVER_FIRST_MESSAGE_VAL.isNil:
20-
SERVER_FIRST_MESSAGE_VAL = cast[ptr Peg](allocShared0(sizeof(Peg)))
21-
SERVER_FIRST_MESSAGE_VAL[] = peg"'r='{[^,]*}',s='{[^,]*}',i='{\d+}$"
22-
SERVER_FIRST_MESSAGE_VAL[]
23-
template SERVER_FINAL_MESSAGE: Peg =
24-
if SERVER_FINAL_MESSAGE_VAL.isNil:
25-
SERVER_FINAL_MESSAGE_VAL = cast[ptr Peg](allocShared0(sizeof(Peg)))
26-
SERVER_FINAL_MESSAGE_VAL[] = peg"'v='{[^,]*}$"
27-
SERVER_FINAL_MESSAGE_VAL[]
28-
else:
29-
let
30-
SERVER_FIRST_MESSAGE = peg"'r='{[^,]*}',s='{[^,]*}',i='{\d+}$"
31-
SERVER_FINAL_MESSAGE = peg"'v='{[^,]*}$"
13+
cbType: ChannelType
14+
cbData: string
3215

3316
proc newScramClient*[T](): ScramClient[T] =
34-
result = new(ScramClient[T])
17+
result = new ScramClient[T]
3518
result.clientNonce = makeNonce()
19+
result.cbType = TLS_NONE
20+
21+
proc setChannelBindingType*[T](s: ScramClient[T], channel: ChannelType) = s.cbType = channel
22+
23+
proc setChannelBindingData*[T](s: ScramClient[T], data: string) = s.cbData = data
3624

3725
proc prepareFirstMessage*(s: ScramClient, username: string): string {.raises: [ScramError]} =
3826
if username.len == 0:
@@ -44,7 +32,7 @@ proc prepareFirstMessage*(s: ScramClient, username: string): string {.raises: [S
4432
s.clientFirstMessageBare.add(s.clientNonce)
4533

4634
s.state = FIRST_PREPARED
47-
GS2_HEADER & s.clientFirstMessageBare
35+
result = makeGS2Header(s.cbType) & s.clientFirstMessageBare
4836

4937
proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: string): string =
5038
if s.state != FIRST_PREPARED:
@@ -53,17 +41,15 @@ proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: st
5341
nonce, salt: string
5442
iterations: int
5543
var matches: array[3, string]
56-
if match(serverFirstMessage, SERVER_FIRST_MESSAGE, matches):
57-
for kv in serverFirstMessage.split(','):
58-
if kv[0..1] == "i=":
59-
iterations = parseInt(kv[2..^1])
60-
elif kv[0..1] == "r=":
61-
nonce = kv[2..^1]
62-
elif kv[0..1] == "s=":
63-
salt = base64.decode(kv[2..^1])
64-
else:
65-
s.state = ENDED
66-
return ""
44+
45+
for kv in serverFirstMessage.split(','):
46+
if kv[0..1] == "i=":
47+
iterations = parseInt(kv[2..^1])
48+
elif kv[0..1] == "r=":
49+
nonce = kv[2..^1]
50+
elif kv[0..1] == "s=":
51+
salt = base64.decode(kv[2..^1])
52+
6753

6854
if not nonce.startsWith(s.clientNonce):
6955
raise newException(ScramError, "Security error: invalid nonce received from server. Possible man-in-the-middle attack.")
@@ -76,7 +62,7 @@ proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: st
7662
clientKey = HMAC[T]($%saltedPassword, CLIENT_KEY)
7763
storedKey = HASH[T]($%clientKey)
7864
serverKey = HMAC[T]($%saltedPassword, SERVER_KEY)
79-
clientFinalMessageWithoutProof = "c=biws,r=" & nonce
65+
clientFinalMessageWithoutProof = makeCBind(s.cbType, s.cbData) & ",r=" & nonce
8066
authMessage =[s.clientFirstMessageBare, serverFirstMessage, clientFinalMessageWithoutProof].join(",")
8167
clientSignature = HMAC[T]($%storedKey, authMessage)
8268
s.serverSignature = HMAC[T]($%serverKey, authMessage)
@@ -93,12 +79,14 @@ proc verifyServerFinalMessage*(s: ScramClient, serverFinalMessage: string): bool
9379
raise newException(ScramError, "You can call this method only once after calling prepareFinalMessage()")
9480
s.state = ENDED
9581
var matches: array[1, string]
96-
if match(serverFinalMessage, SERVER_FINAL_MESSAGE, matches):
97-
var proposedServerSignature: string
98-
for kv in serverFinalMessage.split(','):
99-
if kv[0..1] == "v=":
100-
proposedServerSignature = base64.decode(kv[2..^1])
101-
s.isSuccessful = proposedServerSignature == $%s.serverSignature
82+
83+
var proposedServerSignature: string
84+
for kv in serverFinalMessage.split(','):
85+
if kv[0..1] == "e=":
86+
raise newException(ScramError, "ServerError: " & kv[2..^1])
87+
elif kv[0..1] == "v=":
88+
proposedServerSignature = base64.decode(kv[2..^1])
89+
s.isSuccessful = proposedServerSignature == $%s.serverSignature
10290
s.isSuccessful
10391

10492
proc isSuccessful*(s: ScramClient): bool =

scram/private/types.nim

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,29 @@ type
1414
FIRST_CLIENT_MESSAGE_HANDLED
1515
ENDED
1616

17+
ChannelType* = enum
18+
TLS_NONE = ""
19+
TLS_SERVER_END_POINT = "tls-server-end-point"
20+
TLS_UNIQUE = "tls-unique"
21+
TLS_UNIQUE_FOR_TELNET = "tls-server-for-telnet"
22+
TLS_EXPORT = "tls-export"
23+
24+
ServerError* = enum
25+
SERVER_ERROR_NO_ERROR = ""
26+
SERVER_ERROR_INVALID_ENCODING = "invalid-encoding"
27+
SERVER_ERROR_EXTENSIONS_NOT_SUPPORTED = "extensions-not-supported"
28+
SERVER_ERROR_INVALID_PROOF = "invalid-proof"
29+
SERVER_ERROR_CHANNEL_BINDINGS_DONT_MATCH = "channel-bindings-dont-match"
30+
SERVER_ERROR_SERVER_DOES_SUPPORT_CHANNEL_BINDING = "server-does-support-channel-binding"
31+
SERVER_ERROR_CHANNEL_BINDING_NOT_SUPPORTED = "channel-binding-not-supported"
32+
SERVER_ERROR_UNSUPPORTED_CHANNEL_BINDING_TYPE = "unsupported-channel-binding-type"
33+
SERVER_ERROR_UNKNOWN_USER = "unknown-user"
34+
SERVER_ERROR_INVALID_USERNAME_ENCODING = "invalid-username-encoding"
35+
SERVER_ERROR_NO_RESOURCES = "no-resources"
36+
SERVER_ERROR_OTHER_ERROR = "other-error"
37+
38+
1739
const
18-
GS2_HEADER* = "n,,"
1940
INT_1* = "\x00\x00\x00\x01"
2041
CLIENT_KEY* = "Client Key"
2142
SERVER_KEY* = "Server Key"

scram/private/utils.nim

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,41 @@
1-
import random, base64, strutils, types, hmac, bitops
1+
import random, base64, strutils, types, hmac, bitops, openssl, net, asyncnet
22
from md5 import MD5Digest
33
from sha1 import Sha1Digest
44
from nimSHA2 import Sha224Digest, Sha256Digest, Sha384Digest, Sha512Digest
55

6+
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.}
38+
639
randomize()
740

841
proc `$%`*[T](input: T): string =
@@ -83,4 +116,84 @@ proc hi*[T](password, salt: string, iterations: int): T =
83116
result = previous
84117
for _ in 1..<iterations:
85118
previous = HMAC[T](password, $%previous)
86-
result ^= previous
119+
result ^= previous
120+
121+
proc makeGS2Header*(channel: ChannelType): string =
122+
result = case channel
123+
of TLS_UNIQUE: "p=tls-unique,,"
124+
of TLS_SERVER_END_POINT: "p=tls-server-end-point,,"
125+
of TLS_UNIQUE_FOR_TELNET: "p=tls-server-for-telnet,,"
126+
of TLS_EXPORT: "p=tls-export,,"
127+
else: "n,,"
128+
129+
proc makeCBind*(channel: ChannelType, data: string = ""): string =
130+
if channel == TLS_NONE:
131+
result = "c=biws"
132+
else:
133+
result = "c=" & base64.encode(makeGS2Header(channel) & data)
134+
135+
136+
proc validateChannelBinding*(channel: ChannelType, socket: AnySocket) =
137+
if channel == TLS_NONE:
138+
return
139+
140+
if channel > TLS_EXPORT:
141+
raise newException(ScramError, "Channel type " & $channel & " is not supported")
142+
143+
if socket.isNil:
144+
raise newException(ScramError, "Socket is not initialized")
145+
146+
if not socket.isSsl or socket.sslHandle() == nil:
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/
151+
152+
validateChannelBinding(channel, socket)
153+
154+
result = newString(EVP_MAX_MD_SIZE)
155+
if channel == TLS_UNIQUE:
156+
var ret: csize_t
157+
if isServer:
158+
ret = SSL_get_peer_finished(socket.sslHandle(), result.cstring, EVP_MAX_MD_SIZE)
159+
else:
160+
ret = SSL_get_finished(socket.sslHandle(), result.cstring, EVP_MAX_MD_SIZE)
161+
162+
if ret == 0:
163+
raise newException(ScramError, "SSLError: handshake has not reached the finished message")
164+
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")

0 commit comments

Comments
 (0)