mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
One standard password-hash format everywhere: mux_sha_crypt implements the sha-crypt construction (glibc/musl/openssl-compatible, including rounds= presence, clamping-with-clamped-value-in-output, and 16-char salt truncation) over OS crypto primitives -- OpenSSL EVP on Unix, Windows CNG with a reusable hash handle. mux_crypt routes $5$/$6$ through it on BOTH platforms, so a Unix-written password database verifies on Windows and vice versa, and Unix stops depending on which libc crypt(3) understands those formats (macOS's does not). The libc fall-through remains only for the legacy tail (DES, _-extended, $1$). New hashes are $6$ with an explicit rounds= from the new conf directive password_hash_rounds (default 220000, tracking current OWASP guidance for the SHA-512 PRF; measured ~190 ms/hash on 2022-era server hardware). The check_pass auto-upgrade trigger becomes parameter-aware: a $5$/$6$ hash whose stored rounds differ from policy re-encodes on the next successful login (work-factor migration, automatic), P6H/legacy conversion-on-login is preserved, and the implicit default (password_methods unset) no longer rewrites SHA1-or-stronger hashes -- it used to re-hash every login and would silently downgrade $6$ to $SHA1$ after a config reset. tests/shacrypt (make test-shacrypt, also in test-asan) pins 14 KATs with every golden value from `openssl passwd` as an external oracle, including both published spec vectors. Verified on Windows: 14/14 KATs, digest KATs still 17/17, smoke ALL 1601 PASSED / 0 failed, and a live netmux end-to-end: @pcreate under sha512 policy stores $6$, a $SHA1$ player auto-upgrades on login, a rounds=5000 hash refreshes to 220000 on login, and every Windows/CNG-generated hash re-derives byte-identically with `openssl passwd -6`. Stacked on #1963's CNG backend (same OS-primitives posture and bcrypt.lib link). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
140 lines
5.7 KiB
C++
140 lines
5.7 KiB
C++
/*! \file test_shacrypt.cpp
|
|
* \brief Known-answer tests for the portable sha-crypt implementation (#1962).
|
|
*
|
|
* mux_sha_crypt() must be byte-compatible with glibc/musl/openssl sha-crypt:
|
|
* a $5$/$6$ hash produced on any Unix box must verify on Windows and vice
|
|
* versa — cross-platform password-database compatibility is the point of
|
|
* the implementation, and these vectors are the proof. Every golden value
|
|
* was generated with `openssl passwd -5/-6` as an external oracle (OpenSSL
|
|
* 3.5.6); the first two $6$ vectors are also the published sha-crypt spec
|
|
* vectors.
|
|
*
|
|
* Declares the prototype locally and links the built libmux, following the
|
|
* tests/libmux pattern.
|
|
*
|
|
* Build: make (Unix; needs a built mux/lib/libmux.so)
|
|
* Run: make test (TAP-ish output, nonzero exit on failure)
|
|
*
|
|
* Windows (manual, from this directory, after building the solution):
|
|
* cl /nologo /EHsc /std:c++17 test_shacrypt.cpp /Fe:test_shacrypt.exe ^
|
|
* /link ..\..\mux\bin_release\libmux.lib
|
|
* PATH=..\..\mux\bin_release;%PATH% test_shacrypt.exe
|
|
*/
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
typedef unsigned char UTF8;
|
|
|
|
// shacrypt.h export (declared locally; see file comment)
|
|
const UTF8 *mux_sha_crypt(const UTF8 *szPassword, const UTF8 *szSetting);
|
|
|
|
static int g_tests = 0;
|
|
static int g_failures = 0;
|
|
|
|
static void check(const char *label, const char *password, const char *setting,
|
|
const char *expected)
|
|
{
|
|
++g_tests;
|
|
const UTF8 *got = mux_sha_crypt(reinterpret_cast<const UTF8 *>(password),
|
|
reinterpret_cast<const UTF8 *>(setting));
|
|
bool pass;
|
|
if (nullptr == expected)
|
|
{
|
|
pass = (nullptr == got);
|
|
}
|
|
else
|
|
{
|
|
pass = (nullptr != got
|
|
&& 0 == strcmp(reinterpret_cast<const char *>(got), expected));
|
|
}
|
|
printf("%s %d - %s\n", pass ? "ok" : "not ok", g_tests, label);
|
|
if (!pass)
|
|
{
|
|
printf("# expected %s\n# got %s\n",
|
|
expected ? expected : "(nullptr)",
|
|
got ? reinterpret_cast<const char *>(got) : "(nullptr)");
|
|
++g_failures;
|
|
}
|
|
}
|
|
|
|
int main()
|
|
{
|
|
// ---- $6$ oracle vectors (first two are the published spec vectors) ----
|
|
check("$6$ spec vector: saltstring/password",
|
|
"password", "$6$saltstring",
|
|
"$6$saltstring$adDbXsJjcDlq2662QPgd.tkSOVmnG9Tt3oXl4HR60SusC3AGjirnDe"
|
|
"nVZp3DGwLwqy6iYKCzannhaX9DR72nN1");
|
|
check("$6$ spec vector: rounds=10000/Hello world!",
|
|
"Hello world!", "$6$rounds=10000$saltstringsaltst",
|
|
"$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb"
|
|
"HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.");
|
|
check("$6$ short salt",
|
|
"pw", "$6$shortsl",
|
|
"$6$shortsl$xrC./HXHUqLLcdPesLLPLFcwMb21erO1YPD.QV.FklP707/hvfRcyIRR"
|
|
"QTF2M2GsC2QJLccZQdeDSzl9XNNRv/");
|
|
check("$6$ rounds=1000 (spec minimum)",
|
|
"x", "$6$rounds=1000$minrounds",
|
|
"$6$rounds=1000$minrounds$0S4OIQDq8wIDBzTvucJkohgD79y4P17vzW7GHW33v5"
|
|
"AfLhjx8rcWPvMay6y5x6VFfUrkFcU/RWxIBGwZcNmNL/");
|
|
|
|
// ---- glibc-compatible quirks ----
|
|
check("salt truncates to 16 chars (17 given)",
|
|
"correct horse battery staple", "$6$rounds=100000$exampleSalt16char",
|
|
"$6$rounds=100000$exampleSalt16cha$rJJvkyV/pEXwQxmLEn/N72/nUjl3PbN2D"
|
|
"zPvrdPoiNyUUfQ21PHT/j6t643S8CdaNrAe04NZJqjHanabnKxYU.");
|
|
check("rounds below minimum clamp, clamped value in output",
|
|
"secret", "$6$rounds=999$clampme",
|
|
"$6$rounds=1000$clampme$LJtSQTnZSaC74BWd.2TJdEvuyxftD3ypT5HLCVlvmzUF"
|
|
".sYxR0ERnXp6DVKE9EcMr.aEbYmwndFehPCygNTSm0");
|
|
check("explicit rounds=5000 preserved in output",
|
|
"pw", "$6$rounds=5000$explicitfive",
|
|
"$6$rounds=5000$explicitfive$/eJNoD0.v0Lvex6BCiti2mrWURsa3WNc85C/yH3"
|
|
"4Qj4Uj1jKrfB/cq72HDdFkNVuLDq058n6Uj6WXqUnaWxK0/");
|
|
|
|
// ---- $5$ (verification coverage; generation is $6$-only by policy) ----
|
|
check("$5$ oracle vector: saltstring/password",
|
|
"password", "$5$saltstring",
|
|
"$5$saltstring$OH4IDuTlsuTYPdED1gsuiRMyTAwNlRWyA6Xr3I4/dQ5");
|
|
check("$5$ with rounds",
|
|
"Hello world!", "$5$rounds=10000$saltstringsaltst",
|
|
"$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2."
|
|
"opqey6IcA");
|
|
|
|
// ---- verify-by-recomputation: full stored string as the setting ----
|
|
check("round-trip: stored $6$ string re-verifies byte-identically",
|
|
"password",
|
|
"$6$saltstring$adDbXsJjcDlq2662QPgd.tkSOVmnG9Tt3oXl4HR60SusC3AGjirnDe"
|
|
"nVZp3DGwLwqy6iYKCzannhaX9DR72nN1",
|
|
"$6$saltstring$adDbXsJjcDlq2662QPgd.tkSOVmnG9Tt3oXl4HR60SusC3AGjirnDe"
|
|
"nVZp3DGwLwqy6iYKCzannhaX9DR72nN1");
|
|
{
|
|
const UTF8 *got = mux_sha_crypt(reinterpret_cast<const UTF8 *>("passwerd"),
|
|
reinterpret_cast<const UTF8 *>("$6$saltstring"));
|
|
++g_tests;
|
|
bool pass = (nullptr != got
|
|
&& 0 != strcmp(reinterpret_cast<const char *>(got),
|
|
"$6$saltstring$adDbXsJjcDlq2662QPgd.tkSOVmnG9Tt3oXl4HR"
|
|
"60SusC3AGjirnDenVZp3DGwLwqy6iYKCzannhaX9DR72nN1"));
|
|
printf("%s %d - wrong password does not match stored hash\n",
|
|
pass ? "ok" : "not ok", g_tests);
|
|
if (!pass)
|
|
{
|
|
++g_failures;
|
|
}
|
|
}
|
|
|
|
// ---- malformed settings ----
|
|
check("rejects unknown id $7$", "pw", "$7$salt", nullptr);
|
|
check("rejects non-modular setting", "pw", "XXsalt", nullptr);
|
|
check("rejects truncated prefix", "pw", "$6", nullptr);
|
|
|
|
printf("1..%d\n", g_tests);
|
|
if (g_failures)
|
|
{
|
|
printf("# FAILED: %d of %d\n", g_failures, g_tests);
|
|
return 1;
|
|
}
|
|
printf("# all %d sha-crypt KATs passed\n", g_tests);
|
|
return 0;
|
|
}
|