SCRAM-SHA-256 authentication
Server-side SCRAM-SHA-256 (RFC 5802 / RFC 7677): authenticate a client over a password without the server ever storing — or seeing on the wire — a password-equivalent. The home for a BMC / web console that wants salted, challenge/response browser login instead of sending the password (even over TLS).
At enrollment the server runs axl_scram_sha256_derive and keeps only
{salt, iterations, StoredKey, ServerKey} — never the password. At
login the client proves knowledge of the password through a two-message
challenge/response the server drives with axl_scram_server_first then
axl_scram_server_final; the engine verifies the client proof with a
constant-time compare (axl_consttime_equal) and returns a server
signature the client checks in turn.
This is plain SCRAM-SHA-256 (gs2 header n,,, no channel binding):
a browser client driving it from JavaScript cannot read the TLS
channel-binding hash, so run it inside TLS for transport security. The
two login messages arrive on two separate requests, so AxlScramState
is a plain serializable value the caller parks between the steps.
A matching client engine (axl_scram_client_first /
axl_scram_client_final / axl_scram_client_verify) is provided for
an axl consumer that authenticates to a SCRAM server — a client tool,
agt, or a test driver — including verifying the server’s signature for
mutual authentication. It holds the password only for the exchange and
never sends it.
Built on the dependency-free digest / HMAC / PBKDF2 / base64 / RNG
primitives — available in every build (no AXL_TLS required).
API Reference
Server-side SCRAM-SHA-256 authentication (RFC 5802 / RFC 7677).
SCRAM lets a server authenticate a client over a password without the server ever storing — or seeing on the wire — a password-equivalent. At enrollment the server runs axl_scram_sha256_derive and keeps only {salt, iterations, StoredKey, ServerKey} (an AxlScramCredential); the password is never persisted. At login the client proves knowledge of the password through a two-message challenge/response that the server drives with axl_scram_server_first then axl_scram_server_final.
This is plain SCRAM-SHA-256 only — gs2 header n,,, no channel binding (SCRAM-PLUS). A browser client driving this from JavaScript cannot read the TLS channel-binding hash, so channel binding is out of scope; run it inside TLS for transport security.
// Enrollment (once, when a password is set):
uint8_t salt[16];
axl_rng_bytes(salt, sizeof salt);
uint8_t stored[32], server[32];
axl_scram_sha256_derive("pencil", salt, sizeof salt, 4096, stored, server);
// ... persist {salt, 4096, stored, server} as the user's credential ...
// Login, request 1 (client-first arrives):
AxlScramCredential cred = { salt, sizeof salt, 4096, ..., ... };
AxlScramState st;
char server_first[512];
axl_scram_server_first(&cred, client_first, client_first_len,
server_first, sizeof server_first, &st);
// ... send server_first to the client; park `st` keyed by a login id ...
// Login, request 2 (client-final arrives; reload `st`):
char server_final[256];
if (axl_scram_server_final(&st, client_final, client_final_len,
server_final, sizeof server_final) == AXL_OK) {
// authenticated — send server_final (proves the server too)
}
The two login messages arrive on two separate connections/requests, so AxlScramState is a plain serializable value (no pointers, no heap): copy it into a small table between the two steps and back.
Verification uses axl_consttime_equal (axl-crypto.h) for the proof compare. Dependency-free except for the digest/HMAC/base64/RNG primitives — works in every build (no AXL_TLS required).
Defines
-
AXL_SCRAM_MAX_MESSAGE
Max bytes of a SCRAM message this engine parses/emits (client-first-bare and server-first). Longer inputs are rejected with AXL_INVALID; size an
out_server_firstbuffer at this.
-
AXL_SCRAM_MAX_NONCE
Max length (bytes of printable, comma-free ASCII) of the combined client+server nonce. The client nonce must therefore be at most AXL_SCRAM_MAX_NONCE - AXL_SCRAM_SERVER_NONCE_LEN bytes, else AXL_INVALID.
-
AXL_SCRAM_SERVER_NONCE_LEN
Length of the server nonce the engine appends (base64 of 18 random bytes; base64’s alphabet excludes ‘,’).
-
AXL_SCRAM_SERVER_FINAL_MAX
Minimum size for an
out_server_finalbuffer: “v=” + base64(32-byte ServerSignature) + NUL = 2 + 44 + 1.
Functions
-
int axl_scram_sha256_derive(const char *password, const uint8_t *salt, size_t salt_len, uint32_t iterations, uint8_t stored_key[32], uint8_t server_key[32])
Derive a SCRAM-SHA-256 credential from a password (enrollment).
Computes, per RFC 5802:
SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32)
ClientKey = HMAC-SHA256(SaltedPassword, “Client Key”)
StoredKey = SHA256(ClientKey) (written to
stored_key)ServerKey = HMAC-SHA256(SaltedPassword, “Server Key”) (to
server_key)
The caller persists
{salt, iterations, stored_key, server_key}and discards the password. Use a random per-usersalt(>= 16 bytes) and a highiterations(>= 4096).- Parameters:
password – UTF-8 password (NUL-terminated)
salt – per-user salt (may be NULL iff salt_len==0)
salt_len – salt length in bytes
iterations – PBKDF2 iteration count (>= 1)
stored_key – [out] StoredKey
server_key – [out] ServerKey
- Returns:
AXL_OK on success; AXL_INVALID if
passwordis NULL,iterationsis 0, or an output pointer is NULL; AXL_ERR on an internal failure.
-
int axl_scram_server_first(const AxlScramCredential *cred, const char *client_first, size_t client_first_len, char *out_server_first, size_t out_server_first_size, AxlScramState *out_state)
SCRAM step 1: consume client-first, emit server-first.
Parses the client-first message
"n,,n=<user>,r=<client-nonce>", appends a fresh AXL_SCRAM_SERVER_NONCE_LEN-byte random server nonce to the client nonce, and writes the server-first message"r=<client-nonce‖server-nonce>,s=<base64(salt)>,i=<iterations>"toout_server_first(NUL-terminated). The client-first-message-bare stored for the AuthMessage is"n=<user>,r=<client-nonce>"(the gs2 header is stripped). All state the final step needs is inout_state.Only the plain gs2 header
n,,is accepted: a header requesting or asserting channel binding (y,,,p=...) or carrying an authzid (n,a=...), or a client-first mandatory-extension field (m=...), is rejected with AXL_INVALID — no silent downgrade.- Parameters:
cred – the user’s stored credential
client_first – client-first message
client_first_len – its length in bytes
out_server_first – [out] server-first message
out_server_first_size – capacity of
out_server_firstout_state – [out] state for step 2
- Returns:
AXL_OK on success; AXL_INVALID if the client-first message is malformed, the gs2 header is not exactly
n,,, the client nonce exceeds AXL_SCRAM_MAX_NONCE - AXL_SCRAM_SERVER_NONCE_LEN, the assembled server-first would exceed AXL_SCRAM_MAX_MESSAGE, orout_server_firstis too small; AXL_ERR on an RNG/internal failure.
-
int axl_scram_server_final(const AxlScramState *state, const char *client_final, size_t client_final_len, char *out_server_final, size_t out_server_final_size)
SCRAM step 2: verify client-final, emit server-final.
Parses the client-final message
"c=biws,r=<combined-nonce>,p=<base64(ClientProof)>"(biwsis base64 of the gs2 headern,,), requires its nonce to equal the combined nonce from step 1, and verifies the proof:client-final-without-proof = the client-final message truncated at “,p=” — i.e. the exact bytes
"c=biws,r=<combined-nonce>", INCLUDING the leadingc=biwschannel-binding token.AuthMessage = client-first-bare ‖ “,” ‖ server-first ‖ “,” ‖ client-final-without-proof, where client-first-bare is
"n=<user>,r=<cnonce>"(gs2 header excluded) as captured in step 1.ClientKey’ = ClientProof XOR HMAC-SHA256(StoredKey, AuthMessage)
accept iff SHA256(ClientKey’) == StoredKey (constant-time compare)
On success writes the server-final message
"v=<base64(ServerSignature)>"(ServerSignature = HMAC-SHA256(ServerKey, AuthMessage)) toout_server_final, which the client checks to authenticate the server in turn.A wrong nonce and a wrong proof BOTH return AXL_DENIED — the engine never reveals which failed (distinguishing them would leak whether the login state was valid).
out_server_finalmust be at least AXL_SCRAM_SERVER_FINAL_MAX bytes.- Parameters:
state – state from axl_scram_server_first
client_final – client-final message
client_final_len – its length in bytes
out_server_final – [out] server-final message (>= AXL_SCRAM_SERVER_FINAL_MAX)
out_server_final_size – capacity of
out_server_final
- Returns:
AXL_OK on authentication success; AXL_DENIED if the proof is wrong or the nonce does not match step 1; AXL_INVALID if the client-final message is malformed or
out_server_finalis too small; AXL_ERR on an internal failure.
-
int axl_scram_client_first(const char *username, char *out_client_first, size_t out_size, AxlScramClientState *out_state)
Client step 1: emit the client-first message.
Generates a fresh random client nonce and writes
"n,,n=<username>,r=<client-nonce>"toout_client_first(NUL-terminated). The username must contain no,or=(SCRAM reserves them) and is rejected with AXL_INVALID otherwise.- Parameters:
username – login name
out_client_first – [out] client-first message
out_size – capacity of
out_client_firstout_state – [out] state for the next steps
- Returns:
AXL_OK on success; AXL_INVALID if
usernameis NULL, empty, contains a reserved character, orout_client_firstis too small; AXL_ERR on an RNG failure.
-
int axl_scram_client_final(AxlScramClientState *state, const char *password, const char *server_first, size_t server_first_len, char *out_client_final, size_t out_size)
Client step 2: consume server-first, emit client-final.
Parses the server-first message
"r=<combined>,s=<base64(salt)>,i=<i>", checks thatcombinedbegins with the nonce from step 1 (else the server is not echoing our nonce), derives the proof frompassword, salt and iteration count, and writes the client-final message"c=biws,r=<combined>,p=<base64(ClientProof)>"toout_client_final. It also stores the expected ServerSignature instateforaxl_scram_client_verify.- Parameters:
state – state from axl_scram_client_first
password – UTF-8 password (NUL-terminated)
server_first – server-first message
server_first_len – its length in bytes
out_client_final – [out] client-final message
out_size – capacity of
out_client_final
- Returns:
AXL_OK on success; AXL_INVALID if
passwordis NULL, the server-first message is malformed, its nonce does not extend ours, orout_client_finalis too small; AXL_ERR on an internal failure.
-
int axl_scram_client_verify(const AxlScramClientState *state, const char *server_final, size_t server_final_len)
Client step 3: verify the server-final message (mutual auth).
Parses
"v=<base64(ServerSignature)>"and compares it, in constant time, against the signature computed in step 2 — confirming the server also knows the credential.- Parameters:
state – state from axl_scram_client_final
server_final – server-final message
server_final_len – its length in bytes
- Returns:
AXL_OK if the server signature is correct; AXL_DENIED if it is wrong; AXL_INVALID if the server-final message is malformed.
-
struct AxlScramCredential
- #include <axl-scram.h>
A stored SCRAM credential (no password-equivalent).
What the server persists per user.
saltpoints to the per-user random salt the caller stores (not copied — must outlive anyaxl_scram_server_firstcall that reads it).stored_keyandserver_keycome fromaxl_scram_sha256_derive. None of these reveal the password, andstored_keyalone cannot forge a client proof (that needs ClientKey, which the server never holds).
-
struct AxlScramState
- #include <axl-scram.h>
Engine state carried between the two server steps.
A plain serializable value (no pointers):
axl_scram_server_firstfills it, the caller parks it between the two HTTP requests, andaxl_scram_server_finalconsumes it. Holds the keys, the combined nonce, and the two messages that form the AuthMessage. The salt is NOT retained here — it is already embedded inserver_first(s=...), so the final step needs no salt and the no-pointers POD guarantee holds.It is sensitive (contains the credential keys) but not a password-equivalent. Zero it with
axl_memset(&st, 0, sizeof st)afterserver_final(and likewise anyAxlScramCredentialcopy): a leakedstored_keyenables an offline attack against captured login traffic.Public Members
-
uint8_t stored_key[32]
copied from the credential
-
uint8_t server_key[32]
copied from the credential
-
uint16_t client_first_bare_len
length of client_first_bare
-
uint16_t server_first_len
length of server_first
-
uint16_t combined_nonce_len
length of combined_nonce
-
char client_first_bare[512]
“n=<user>,r=<cnonce>”
-
char server_first[512]
“r=…,s=…,i=…”
-
char combined_nonce[160]
client nonce ‖ server nonce
-
uint8_t stored_key[32]
-
struct AxlScramClientState
- #include <axl-scram.h>
Client state carried across the SCRAM exchange.
A plain serializable value (no pointers):
axl_scram_client_firstfills the nonce/message fields,axl_scram_client_finaladds the expected server signature, andaxl_scram_client_verifychecks it. It holds no password-equivalent. Zero it withaxl_memset(&st, 0, sizeof st)after use.