mirror of https://github.com/sysown/proxysql
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
8.1 KiB
169 lines
8.1 KiB
#include "test_globals.h"
|
|
#include "test_init.h"
|
|
#include "PgSQL_Backend_Protocol.h"
|
|
#include <cstring>
|
|
#include <string>
|
|
#include "scram.h" // libscram: used to pin the RFC vector and to act as an independent SCRAM verifier
|
|
#include "tap.h"
|
|
|
|
int main(int, char**) {
|
|
plan(7);
|
|
|
|
// SSLRequest is a fixed 8 bytes: length=8, code=80877103 (0x04d2162f).
|
|
unsigned char ssl[8];
|
|
pg_build_ssl_request(ssl);
|
|
unsigned char expect_ssl[8] = {0x00,0x00,0x00,0x08, 0x04,0xd2,0x16,0x2f};
|
|
ok(memcmp(ssl, expect_ssl, 8) == 0, "SSLRequest bytes exact");
|
|
|
|
// Startup message: int32 length, int32 protocol 196608 (3.0), then key\0value\0... \0.
|
|
unsigned char sm[256]; size_t smlen = 0;
|
|
pg_build_startup(sm, &smlen, sizeof(sm), "alice", "shop");
|
|
// protocol version at offset 4 must be 0x00030000
|
|
ok(sm[4]==0x00 && sm[5]==0x03 && sm[6]==0x00 && sm[7]==0x00, "startup protocol 3.0");
|
|
|
|
// AuthenticationMD5Password response: "md5" + hex(md5(hex(md5(pass+user))+salt)).
|
|
// Known vector: user=postgres, password=postgres, salt={1,2,3,4} (independent python ref).
|
|
char md5buf[36];
|
|
unsigned char salt[4] = {0x01,0x02,0x03,0x04};
|
|
pg_build_md5(md5buf, "postgres", "postgres", salt);
|
|
ok(strcmp(md5buf, "md568be9ed08db75f318087ab337aaea044") == 0, "md5 response matches reference vector");
|
|
|
|
// --- SCRAM-SHA-256 ---
|
|
|
|
// (4) Wrapper shape: client-first must carry the gs2 'n' header ("n,,") and a nonce
|
|
// ("r="), with an empty SCRAM username field ("n=") per the PostgreSQL convention.
|
|
{
|
|
PgSQL_Scram_State* s = pg_scram_new();
|
|
const char* cf = pg_scram_client_first(s, /*channel_binding=*/false);
|
|
bool shape_ok = cf != nullptr
|
|
&& strncmp(cf, "n,,", 3) == 0 // gs2 header: no channel binding
|
|
&& strstr(cf, "n=,") != nullptr // empty username field
|
|
&& strstr(cf, ",r=") != nullptr; // client nonce present
|
|
ok(shape_ok, "pg_scram_client_first: gs2 'n,,' header, empty n=, and r= nonce (got: %s)",
|
|
cf ? cf : "(null)");
|
|
pg_scram_free(s);
|
|
}
|
|
|
|
// (5) Proof correctness, RFC 7677 Section 5 SCRAM-SHA-256 test vector.
|
|
// username "user", password "pencil", client nonce "rOprNGfwEbeRWgbNEkqO".
|
|
// Expected client-final proof: p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=
|
|
//
|
|
// The RFC vector uses client-first-bare "n=user,r=..." (a non-empty username), so we
|
|
// pin it by driving libscram directly with the RFC ScramState fields rather than via
|
|
// build_client_first_message (which would generate a random nonce and an EMPTY n=).
|
|
// build_client_final_message consumes client_nonce, client_first_message_bare,
|
|
// server_first_message and cbind_flag to recompute the proof.
|
|
{
|
|
ScramState* st = scram_state_init();
|
|
st->client_nonce = strdup("rOprNGfwEbeRWgbNEkqO");
|
|
st->client_first_message_bare = strdup("n=user,r=rOprNGfwEbeRWgbNEkqO");
|
|
st->server_first_message = strdup(
|
|
"r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096");
|
|
st->cbind_flag = 'n';
|
|
|
|
PgCredentials creds{};
|
|
snprintf(creds.passwd, sizeof(creds.passwd), "%s", "pencil");
|
|
creds.has_scram_keys = false;
|
|
|
|
const char* server_nonce = "rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0";
|
|
const char* salt_b64 = "W22ZaJ0SNY7soEsUEjb6gQ==";
|
|
// RFC salt decodes from base64; libscram's read path decodes it, but here we call
|
|
// build_client_final_message directly which takes the RAW (decoded) salt.
|
|
unsigned char salt_raw[64];
|
|
// Decode "W22ZaJ0SNY7soEsUEjb6gQ==" -> 16 bytes (standard base64, no helper here).
|
|
// Hand-decode via libscram is not exposed; use a tiny inline base64 decoder.
|
|
auto b64val = [](char c) -> int {
|
|
if (c >= 'A' && c <= 'Z') return c - 'A';
|
|
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
|
if (c >= '0' && c <= '9') return c - '0' + 52;
|
|
if (c == '+') return 62;
|
|
if (c == '/') return 63;
|
|
return -1; // '=' padding or invalid
|
|
};
|
|
int saltlen = 0;
|
|
{
|
|
int bits = 0, acc = 0;
|
|
for (const char* p = salt_b64; *p; ++p) {
|
|
int v = b64val(*p);
|
|
if (v < 0) break; // padding terminates
|
|
acc = (acc << 6) | v; bits += 6;
|
|
if (bits >= 8) { bits -= 8; salt_raw[saltlen++] = (acc >> bits) & 0xff; }
|
|
}
|
|
}
|
|
|
|
char* final_msg = build_client_final_message(
|
|
st, &creds, server_nonce, (const char*)salt_raw, saltlen, 4096);
|
|
|
|
bool proof_ok = final_msg != nullptr
|
|
&& strstr(final_msg, "p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=") != nullptr;
|
|
ok(proof_ok, "client-final proof matches RFC 7677 Section 5 vector (got: %s)",
|
|
final_msg ? final_msg : "(null)");
|
|
|
|
free(final_msg);
|
|
free_scram_state(st);
|
|
}
|
|
|
|
// (6) Full client<->server round trip exercising the WRAPPERS under the PostgreSQL
|
|
// empty-username convention (n=,...). The client side is driven entirely through the
|
|
// pg_scram_* wrappers; the server side is libscram's independent SCRAM verifier. This
|
|
// proves the proof our wrapper computes is ACCEPTED by an independent implementation,
|
|
// which the RFC pin (which uses n=user and bypasses the wrappers) cannot show.
|
|
{
|
|
const char* password = "s3cr3t-passw0rd";
|
|
|
|
// --- client: build client-first via the wrapper ---
|
|
PgSQL_Scram_State* client = pg_scram_new();
|
|
const char* client_first = pg_scram_client_first(client, /*channel_binding=*/false);
|
|
|
|
// --- server: parse client-first and build server-first (libscram, independent) ---
|
|
ScramState* server = scram_state_init();
|
|
std::string cf_copy(client_first); // read_client_first_message mutates its input
|
|
char cbind_flag = 0;
|
|
char* cfmb = nullptr;
|
|
char* cnonce = nullptr;
|
|
bool parsed = read_client_first_message(&cf_copy[0], &cbind_flag, &cfmb, &cnonce);
|
|
server->cbind_flag = cbind_flag;
|
|
server->client_first_message_bare = cfmb; // ownership transferred to server state
|
|
server->client_nonce = cnonce;
|
|
// Plaintext password as the "stored secret" -> libscram derives an ad-hoc verifier.
|
|
char* server_first = parsed ? build_server_first_message(server, "", password) : nullptr;
|
|
|
|
// --- client: build client-final (WITH proof) via the wrapper ---
|
|
const char* client_final = server_first
|
|
? pg_scram_client_final(client, password, server_first, strlen(server_first))
|
|
: nullptr;
|
|
|
|
// --- server: verify nonce + client proof (independent verifier) ---
|
|
bool accepted = false;
|
|
if (client_final) {
|
|
// read_client_final_message takes a pristine raw_input (for the without-proof
|
|
// reconstruction) AND a separate mutable input buffer it overwrites with NULs;
|
|
// they must be distinct copies (mirrors PgSQL_Protocol::scram_handle_client_final).
|
|
std::string raw(client_final);
|
|
std::string finbuf(client_final);
|
|
const char* final_nonce = nullptr;
|
|
char* proof = nullptr;
|
|
bool rf = read_client_final_message(server, (const uint8_t*)raw.c_str(), &finbuf[0],
|
|
&final_nonce, &proof);
|
|
if (rf) {
|
|
accepted = verify_final_nonce(server, final_nonce)
|
|
&& verify_client_proof(server, proof);
|
|
}
|
|
free(proof);
|
|
}
|
|
ok(accepted, "wrapper client proof accepted by independent libscram server verifier");
|
|
|
|
// --- bonus: client verifies the server's final signature via the wrapper ---
|
|
char* server_final = accepted ? build_server_final_message(server) : nullptr;
|
|
bool server_verified = server_final
|
|
&& pg_scram_verify_server_final(client, server_final, strlen(server_final));
|
|
ok(server_verified, "wrapper verifies server-final signature (mutual auth round trip)");
|
|
|
|
free(server_final);
|
|
free_scram_state(server);
|
|
pg_scram_free(client);
|
|
}
|
|
|
|
return exit_status();
|
|
}
|