Skip to content

Commit cffd4fd

Browse files
authored
Add the SCRAM mechanism. (#5)
1 parent 75db064 commit cffd4fd

12 files changed

Lines changed: 2272 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
0.2.0 (2026-03-22)
55
------------------
66
* Fix CRAM-MD5 server challenge to store the encoded challenge string (e.g. `<nonce>`) in the SASL context rather than the raw nonce, so the password callable receives the value the client used for its HMAC per RFC 2195.
7+
* Add SCRAM support (RFC 5802 / RFC 7677) covering all standard variants: SCRAM-SHA-1, SCRAM-SHA-224, SCRAM-SHA-256, SCRAM-SHA-384, SCRAM-SHA-512, SCRAM-SHA3-512, and their respective `-PLUS` channel-binding counterparts.
78

89
0.1.1 (2019-12-09)
910
------------------

docs/Mechanisms.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# SASL Mechanisms
2+
3+
This library provides the following SASL mechanisms. All mechanisms are registered by default; use the `supported` option to restrict which are available.
4+
5+
```php
6+
use FreeDSx\Sasl\Sasl;
7+
8+
$sasl = new Sasl();
9+
10+
// Restrict to specific mechanisms
11+
$sasl = new Sasl(['supported' => ['SCRAM-SHA-256', 'PLAIN']]);
12+
13+
// Get a specific mechanism
14+
$mechanism = $sasl->get('SCRAM-SHA-256');
15+
16+
// Select the best mechanism from a server-advertised list
17+
$mechanism = $sasl->select(['SCRAM-SHA-256', 'PLAIN', 'DIGEST-MD5']);
18+
```
19+
20+
---
21+
22+
## ANONYMOUS
23+
24+
No authentication. Sends optional trace information to the server.
25+
26+
**Security:** No integrity, no privacy, no authentication.
27+
28+
**Client options:**
29+
30+
| Option | Default | Description |
31+
|------------|---------|------------------------------------------|
32+
| `username` | `null` | Optional trace string sent to the server |
33+
34+
```php
35+
$challenge = $mechanism->challenge();
36+
37+
$response = $challenge->challenge(
38+
null,
39+
['username' => 'guest@example.com']
40+
);
41+
42+
// $response->isComplete() === true after one round
43+
```
44+
45+
---
46+
47+
## PLAIN
48+
49+
Sends credentials as plaintext. Use only over TLS.
50+
51+
**Security:** Authenticates but transmits password in plaintext.
52+
53+
**Client options:**
54+
55+
| Option | Default | Description |
56+
|------------|--------------|-------------------------|
57+
| `username` | *(required)* | Authentication identity |
58+
| `password` | *(required)* | User password |
59+
60+
**Server options:**
61+
62+
| Option | Default | Description |
63+
|------------|--------------|----------------------------------------------------------------------|
64+
| `validate` | *(required)* | `callable(string $authzid, string $authcid, string $password): bool` |
65+
66+
```php
67+
// Client
68+
$response = $challenge->challenge(null, [
69+
'username' => 'user',
70+
'password' => 'secret',
71+
]);
72+
73+
// Server
74+
$response = $challenge->challenge($received, [
75+
'validate' => fn($authzid, $authcid, $password) => $password === getPassword($authcid),
76+
]);
77+
```
78+
79+
---
80+
81+
## CRAM-MD5
82+
83+
Server sends a challenge; a client responds with an HMAC-MD5 digest. Two-round exchange.
84+
85+
**Security:** Authenticates without transmitting the password in plaintext.
86+
87+
**Client options:**
88+
89+
| Option | Default | Description |
90+
|------------|--------------|---------------|
91+
| `username` | *(required)* | Username |
92+
| `password` | *(required)* | User password |
93+
94+
**Server options:**
95+
96+
| Option | Default | Description |
97+
|-------------|----------------------|-----------------------------------------------------------------------------------|
98+
| `challenge` | random 32-byte nonce | Override the server challenge string |
99+
| `password` | *(required)* | `callable(string $username, string $challenge): string` — returns expected digest |
100+
101+
```php
102+
// Client (round 1: receive challenge, round 2: send response)
103+
$response = $challenge->challenge($serverChallenge, [
104+
'username' => 'user',
105+
'password' => 'secret',
106+
]);
107+
```
108+
109+
---
110+
111+
## DIGEST-MD5
112+
113+
Multi-round MD5-based authentication with optional integrity and privacy security layers. Implements RFC 2831.
114+
115+
**Security:** Authenticates without plaintext password. Optionally provides integrity (`auth-int`) or privacy (`auth-conf`) security layers.
116+
117+
**Client options:**
118+
119+
| Option | Default | Description |
120+
|-----------------|--------------|---------------------------------------------------|
121+
| `username` | *(required)* | Username |
122+
| `password` | *(required)* | User password |
123+
| `host` | `null` | Hostname for the digest-uri |
124+
| `realm` | `null` | Authentication realm |
125+
| `use_integrity` | `false` | Enable integrity layer (`qop=auth-int`) |
126+
| `use_privacy` | `false` | Enable privacy/encryption layer (`qop=auth-conf`) |
127+
| `service` | `'ldap'` | SASL service name |
128+
| `cnonce` | random | Client nonce |
129+
| `cipher` | auto | Cipher for privacy (e.g. `'rc4'`, `'3des'`) |
130+
131+
**Server options:**
132+
133+
| Option | Default | Description |
134+
|-----------------|--------------|------------------------------------------|
135+
| `validate` | *(required)* | Callable to validate the client response |
136+
| `use_integrity` | `false` | Offer integrity layer |
137+
| `use_privacy` | `false` | Offer privacy layer |
138+
| `service` | `'ldap'` | SASL service name |
139+
| `nonce` | random | Override the server nonce |
140+
141+
```php
142+
$response = $challenge->challenge($serverMessage, [
143+
'username' => 'user',
144+
'password' => 'secret',
145+
'host' => 'ldap.example.com',
146+
'service' => 'ldap',
147+
]);
148+
```
149+
150+
---
151+
152+
## SCRAM-SHA-* (RFC 5802 / RFC 7677)
153+
154+
Modern salted challenge-response mechanisms using PBKDF2 key derivation and HMAC signatures. Mutual authentication: the client also verifies the server. `-PLUS` variants add TLS channel binding.
155+
156+
**Available mechanisms:**
157+
158+
| Mechanism | Hash Algorithm | Channel Binding |
159+
|-----------------------|----------------|-----------------|
160+
| `SCRAM-SHA-1` | SHA-1 | No |
161+
| `SCRAM-SHA-1-PLUS` | SHA-1 | Yes |
162+
| `SCRAM-SHA-224` | SHA-224 | No |
163+
| `SCRAM-SHA-224-PLUS` | SHA-224 | Yes |
164+
| `SCRAM-SHA-256` | SHA-256 | No |
165+
| `SCRAM-SHA-256-PLUS` | SHA-256 | Yes |
166+
| `SCRAM-SHA-384` | SHA-384 | No |
167+
| `SCRAM-SHA-384-PLUS` | SHA-384 | Yes |
168+
| `SCRAM-SHA-512` | SHA-512 | No |
169+
| `SCRAM-SHA-512-PLUS` | SHA-512 | Yes |
170+
| `SCRAM-SHA3-512` | SHA3-512 | No |
171+
| `SCRAM-SHA3-512-PLUS` | SHA3-512 | Yes |
172+
173+
**Security:** Authenticates without transmitting the password. Resistant to replay and server-impersonation attacks. Use `-PLUS` variants with TLS channel binding for the strongest guarantee.
174+
175+
### Client options
176+
177+
**Round 1 (client-first):**
178+
179+
| Option | Default | Description |
180+
|--------------|-----------------|-------------------------------------------------|
181+
| `username` | *(required)* | Username (`=` and `,` are encoded per RFC 5802) |
182+
| `cnonce` | 24 random bytes | Client nonce |
183+
| `cbind_type` | `'tls-unique'` | Channel binding type (for `-PLUS` variants) |
184+
185+
**Round 2 (client-final):**
186+
187+
| Option | Default | Description |
188+
|--------------|--------------|-------------------------------------------------|
189+
| `password` | *(required)* | User password |
190+
| `cbind_data` | `''` | Raw channel binding data (for `-PLUS` variants) |
191+
192+
### Server options
193+
194+
**Round 1 (server-first):**
195+
196+
| Option | Default | Description |
197+
|--------------|-----------------|--------------------------------------|
198+
| `nonce` | 24 random bytes | Server portion of the combined nonce |
199+
| `salt` | 16 random bytes | PBKDF2 salt (raw binary) |
200+
| `iterations` | `4096` | PBKDF2 iteration count |
201+
202+
**Round 2 (server-final):**
203+
204+
| Option | Default | Description |
205+
|------------|--------------|------------------------------------------|
206+
| `password` | *(required)* | User password to verify the client proof |
207+
208+
### Example (client)
209+
210+
```php
211+
$mechanism = $sasl->get('SCRAM-SHA-256');
212+
$challenge = $mechanism->challenge();
213+
214+
// Round 1: send client-first message
215+
$response = $challenge->challenge(null, ['username' => 'user']);
216+
$clientFirst = $response->get('response');
217+
218+
// Round 2: receive server-first, send client-final with password
219+
$response = $challenge->challenge($serverFirst, ['password' => 'secret']);
220+
$clientFinal = $response->get('response');
221+
222+
// Round 3: receive server-final, verify server signature
223+
$response = $challenge->challenge($serverFinal, []);
224+
// $response->isComplete() === true
225+
```
226+
227+
---
228+
229+
## Security Strength Summary
230+
231+
| Mechanism | Authentication | Integrity Layer | Privacy Layer | Plaintext Password |
232+
|-------------|:--------------:|:---------------:|:-------------:|:------------------:|
233+
| ANONYMOUS | No | No | No | No |
234+
| PLAIN | Yes | No | No | **Yes** |
235+
| CRAM-MD5 | Yes | No | No | No |
236+
| DIGEST-MD5 | Yes | Optional | Optional | No |
237+
| SCRAM-SHA-* | Yes (mutual) | No | No | No |
238+
239+
For new integrations, prefer `SCRAM-SHA-256` or higher over `PLAIN`, `CRAM-MD5`, and `DIGEST-MD5`.
240+
241+
Always use SCRAM or PLAIN over a TLS-protected connection.

0 commit comments

Comments
 (0)