Harden SSH Admission And Credential Input
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# SSH authentication transport host regression
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
CCACHE_DISABLE=1 python3 tests/ssh_auth_transport/run.py
|
||||
```
|
||||
|
||||
Requires Python 3 and a C11 host compiler (`cc`, or `CC` override). The runner also sets `CCACHE_DISABLE=1` for its subprocesses. Compilation uses `-Wall -Wextra -Werror`; compilation and execution have timeouts. Generated C and executable live in a temporary directory, not the source tree. Assertion or compiler failures propagate as a nonzero exit.
|
||||
|
||||
## Production code under test
|
||||
|
||||
Following `tests/ssh_management/runtime.py`, `run.py` extracts named function definitions **verbatim** from `src/ssh_transport.c`. It extracts the actual slot definition, transport constants, session/route enums and counters declaration too, and links the actual `src/ssh_auth_policy.c` with its header. Missing/ambiguous function definitions fail extraction rather than silently falling back to fixture implementations.
|
||||
|
||||
The extracted set covers authentication callbacks and helpers, counter saturation/clear, socket acceptance and free-slot selection, start/stop, retirement, admin RX, serial RX and shared TX flushing. The fixture supplies only synchronous boundary doubles for clock, database, socket/library allocation and I/O, broker, console, publication and context/listener construction. No credential verifier, admission algorithm or callback logic is reimplemented. `boot()` resets test state between independent scenarios; the restart tests use actual production start/stop without resetting the policy.
|
||||
|
||||
## Coverage
|
||||
|
||||
- Password success, invalid password, backend failure and rejected password change; admissions versus completed attempts, principal promotion and pending-principal clearing.
|
||||
- Signed key authorization denial/backend error, bad signature result, stale principal, currentness backend failure and success. Admission precedes authorization, authorized signed keys await completion, and duplicate/unexpected callbacks cannot create a promotion or count again. Verification-throttled signed requests do not reach authorization or acquire a pending signature-result marker.
|
||||
- Close/state/pending-result fences, unsupported method handling, keyboard callback rejection with the entire prompt structure cleared and no repeated method count after close.
|
||||
- Unsigned authorized/rejected probes share a 12-token pool across both slots/reconnects, refill one token at five seconds and never become completed attempts. Password/signed verification shares six tokens, refilling one per ten seconds. Three completed failed password or signed-key attempts close the slot.
|
||||
- Actual acceptance loop: two-slot capacity, six handshake admissions, allocation failures consume admission, throttle denial before allocation, and one-token/ten-second refill.
|
||||
- Actual stop/start and counter clear preserve all three exhausted pools. Counter clear initialization guard, 64-bit saturating addition and per-slot 8-bit saturation.
|
||||
- Partial admin RX consumption (including `false` with consumed bytes), zero-consumption rejection and full drain wipe only consumed spans. WANT_READ, WANT_WRITE, rekey, window/channel retry, zero and error paths preserve pending TX; positive partial sends wipe only accepted admin spans. Binary serial RX/TX remains unchanged, including embedded NUL/0xff and partial broker acceptance.
|
||||
- Whole-slot retirement wipe including padding and buffers, exact retained generation, `socket_fd == -1` sentinel, broker-disconnect failure retaining a closing slot, and repeated cleanup without double free/close. The old live fd is closed, not retained.
|
||||
- Source checks for context registration of authentication, type advertisement, result and rejecting keyboard callbacks, plus per-session auth/result/keyboard contexts.
|
||||
|
||||
## Limits and deferred validation
|
||||
|
||||
The wolfSSH types in `fixture.h` are narrow host shapes, not ABI validation. Library return codes model distinct outcomes; this does not compile wolfSSH or perform cryptographic signature work. Denied authorization is verified to return a non-success result and leave no pending completion; proving the parser actually skips signature processing belongs to the separately delegated real wolfSSH parser-order regression. Do not treat these tests as its replacement.
|
||||
|
||||
Context construction is stubbed; callback registration is checked in production source rather than executed. Console/broker/database behavior and real socket scheduling are outside this suite. A fixture principal uses synthetic bytes to inspect lifecycle clearing; there are no real credentials.
|
||||
|
||||
No firmware build, device execution, upload or erase is performed. All device validation remains deferred to whole Phase 9, including real clients, timing, concurrency, recovery and physical UART/broker behavior.
|
||||
@@ -0,0 +1,142 @@
|
||||
/* Host-only boundary doubles. No authentication/policy/transport logic here. */
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include "ssh_auth_policy.h"
|
||||
typedef unsigned char byte;
|
||||
typedef uint32_t word32;
|
||||
typedef int esp_err_t;
|
||||
typedef int WOLFSSH;
|
||||
typedef int WOLFSSH_CTX;
|
||||
typedef unsigned session_broker_client_id_t;
|
||||
typedef struct { uint32_t id; unsigned char secret[28]; } user_principal_t;
|
||||
typedef struct { unsigned slot; } admin_ssh_console_token_t;
|
||||
typedef struct {
|
||||
byte isCert, hasSignature;
|
||||
const byte *publicKeyType, *publicKey;
|
||||
word32 publicKeyTypeSz, publicKeySz;
|
||||
} WS_UserAuthData_PublicKey;
|
||||
typedef struct {
|
||||
byte type;
|
||||
const byte *username;
|
||||
word32 usernameSz;
|
||||
union {
|
||||
struct { byte hasNewPassword; const byte *password; word32 passwordSz; } password;
|
||||
WS_UserAuthData_PublicKey publicKey;
|
||||
} sf;
|
||||
} WS_UserAuthData;
|
||||
typedef struct { unsigned promptCount; void *prompts; byte storage[32]; } WS_UserAuthData_Keyboard;
|
||||
enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_STATE, ESP_ERR_TIMEOUT, ESP_ERR_NOT_FOUND };
|
||||
enum { WOLFSSH_USERAUTH_PASSWORD=1, WOLFSSH_USERAUTH_PUBLICKEY=2,
|
||||
WOLFSSH_USERAUTH_SUCCESS=0, WOLFSSH_USERAUTH_REJECTED=3,
|
||||
WOLFSSH_USERAUTH_INVALID_AUTHTYPE, WOLFSSH_USERAUTH_INVALID_PASSWORD,
|
||||
WOLFSSH_USERAUTH_FAILURE, WOLFSSH_USERAUTH_INVALID_PUBLICKEY };
|
||||
enum { WS_SUCCESS=0, WS_ERROR=-1, WS_WANT_READ=-2, WS_WANT_WRITE=-3,
|
||||
WS_REKEYING=-4, WS_WINDOW_FULL=-5, WS_CHAN_RXD=-6 };
|
||||
#define SESSION_BROKER_NO_CLIENT 0U
|
||||
/* PRODUCTION TYPES */
|
||||
static ssh_slot_t s_slots[SSH_TRANSPORT_MAX_SESSIONS];
|
||||
static ssh_transport_counters_t s_counters;
|
||||
static ssh_auth_policy_t s_auth_policy;
|
||||
static WOLFSSH_CTX *s_context;
|
||||
static int s_listen_fd=-1, s_lock;
|
||||
static bool s_initialized;
|
||||
static unsigned lock_depth, shutdown_calls, close_calls, password_calls, key_calls;
|
||||
static unsigned current_calls, allocations, frees, context_frees;
|
||||
static int64_t now_us;
|
||||
static bool db_accept, db_current, feed_ok, accepts_input=true, allocation_fail;
|
||||
static esp_err_t db_error, current_error, broker_error;
|
||||
static size_t feed_consumed, broker_accepted;
|
||||
static int send_result, read_result, ssh_error, accepts_remaining;
|
||||
static const byte payload[] = {0x00, 0xff, 'p', 'a', 's', 's', '\r', '\n'};
|
||||
static const void *wipe_address;
|
||||
static size_t wipe_size;
|
||||
#define taskENTER_CRITICAL(p) do { (void)(p); assert(lock_depth++ == 0); } while (0)
|
||||
#define taskEXIT_CRITICAL(p) do { (void)(p); assert(--lock_depth == 0); } while (0)
|
||||
#define pdMS_TO_TICKS(n) (n)
|
||||
static void secure_wipe(void *p, size_t n) {
|
||||
wipe_address=p; wipe_size=n;
|
||||
volatile byte *b=p; while(n--) *b++=0;
|
||||
}
|
||||
static int64_t esp_timer_get_time(void) { return now_us; }
|
||||
static int fake_shutdown(int fd, int how) { assert(fd>=0 && how==SHUT_RDWR); ++shutdown_calls; return 0; }
|
||||
static int fake_close(int fd) { assert(fd>=0); ++close_calls; return 0; }
|
||||
#define shutdown fake_shutdown
|
||||
#define close fake_close
|
||||
static esp_err_t user_database_authenticate_password(const byte *u, word32 un,
|
||||
const byte *p, word32 pn, user_principal_t *out, bool *ok) {
|
||||
assert(u && un==1 && p && pn==sizeof(payload)); ++password_calls;
|
||||
memset(out, 0x5a, sizeof(*out)); *ok=db_accept; return db_error;
|
||||
}
|
||||
static esp_err_t user_database_authorize_ssh_public_key(const byte *u, word32 un,
|
||||
const byte *t, word32 tn, const byte *k, word32 kn, user_principal_t *out, bool *ok) {
|
||||
assert(u && un==1 && t && tn==1 && k && kn==sizeof(payload)); ++key_calls;
|
||||
memset(out, 0x5a, sizeof(*out)); *ok=db_accept; return db_error;
|
||||
}
|
||||
static esp_err_t user_database_principal_is_current(const user_principal_t *p, bool *ok) {
|
||||
assert(p->id==0x5a5a5a5a); ++current_calls; *ok=db_current; return current_error;
|
||||
}
|
||||
static admin_ssh_console_token_t admin_console_token(ssh_slot_t *s, size_t i) {
|
||||
assert(s==&s_slots[i]); return (admin_ssh_console_token_t){(unsigned)i};
|
||||
}
|
||||
static void admin_ssh_console_close(const admin_ssh_console_token_t *t) { assert(t->slot<2); }
|
||||
static int wolfSSH_shutdown(WOLFSSH *s) { assert(s); return 0; }
|
||||
static void wolfSSH_free(WOLFSSH *s) { assert(s); ++frees; }
|
||||
static esp_err_t session_broker_disconnect(session_broker_client_id_t id) { assert(id); return broker_error; }
|
||||
static void publish_slot(ssh_slot_t *s, size_t i) { assert(s==&s_slots[i]); }
|
||||
static void vTaskDelay(unsigned n) { (void)n; }
|
||||
static esp_err_t create_context(void) { static WOLFSSH_CTX ctx; s_context=&ctx; return ESP_OK; }
|
||||
static esp_err_t create_listener(void) { s_listen_fd=10; return ESP_OK; }
|
||||
static void wolfSSH_CTX_free(WOLFSSH_CTX *c) { assert(c); ++context_frees; }
|
||||
static int fake_accept(int fd, struct sockaddr *p, socklen_t *n) {
|
||||
assert(fd==10); memset(p,0,*n);
|
||||
if(accepts_remaining>0) { --accepts_remaining; return 20+accepts_remaining; }
|
||||
errno=EAGAIN; return -1;
|
||||
}
|
||||
#define accept fake_accept
|
||||
static int fake_setsockopt(int f,int l,int o,const void *v,socklen_t n) {
|
||||
(void)f;(void)l;(void)o;(void)v;(void)n; return 0;
|
||||
}
|
||||
#define setsockopt fake_setsockopt
|
||||
static esp_err_t set_nonblocking(int fd) { assert(fd>=0); return ESP_OK; }
|
||||
static void format_peer(const struct sockaddr_storage *p,char *out,size_t n) {
|
||||
(void)p; assert(n>4); strcpy(out,"host");
|
||||
}
|
||||
static WOLFSSH *wolfSSH_new(WOLFSSH_CTX *c) { static WOLFSSH ssh; assert(c); ++allocations; return allocation_fail ? NULL : &ssh; }
|
||||
static int wolfSSH_set_fd(WOLFSSH *s,int fd) { assert(s && fd>=0); return WS_SUCCESS; }
|
||||
#define CONTEXT_SETTER(name) static void name(WOLFSSH *s,void *p) { assert(s && p); }
|
||||
CONTEXT_SETTER(wolfSSH_SetIOReadCtx)
|
||||
CONTEXT_SETTER(wolfSSH_SetUserAuthCtx)
|
||||
CONTEXT_SETTER(wolfSSH_SetUserAuthResultCtx)
|
||||
CONTEXT_SETTER(wolfSSH_SetKeyboardAuthCtx)
|
||||
CONTEXT_SETTER(wolfSSH_SetChannelReqCtx)
|
||||
static int wolfSSH_get_error(WOLFSSH *s) { assert(s); return ssh_error; }
|
||||
static int wolfSSH_stream_send(WOLFSSH *s,const byte *p,word32 n) {
|
||||
assert(s && n && (send_result<=0 || (unsigned)send_result<=n));
|
||||
assert(memcmp(p,payload+sizeof(payload)-n,n)==0); return send_result;
|
||||
}
|
||||
static int wolfSSH_stream_read(WOLFSSH *s,byte *p,word32 n) {
|
||||
assert(s && n>=sizeof(payload));
|
||||
if(read_result>0) { assert(read_result==(int)sizeof(payload)); memcpy(p,payload,sizeof(payload)); }
|
||||
return read_result;
|
||||
}
|
||||
static bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *t,
|
||||
const byte *p,size_t n,size_t *consumed) {
|
||||
assert(t->slot<2 && feed_consumed<=n);
|
||||
assert(memcmp(p,payload+sizeof(payload)-n,n)==0);
|
||||
*consumed=feed_consumed; return feed_ok;
|
||||
}
|
||||
static bool admin_ssh_console_accepts_input(const admin_ssh_console_token_t *t) { assert(t->slot<2); return accepts_input; }
|
||||
static esp_err_t session_broker_write(session_broker_client_id_t id,const byte *p,
|
||||
size_t n,size_t *accepted) {
|
||||
(void)id; assert(broker_accepted<=n);
|
||||
assert(memcmp(p,payload+sizeof(payload)-n,n)==0);
|
||||
*accepted=broker_accepted; return broker_error;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile verbatim transport functions with narrow host boundary doubles."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
SOURCE = (ROOT / 'src/ssh_transport.c').read_text()
|
||||
HEADER = (ROOT / 'src/ssh_transport.h').read_text()
|
||||
|
||||
|
||||
def function(name):
|
||||
matches = list(re.finditer(r'^(?:static )?[^\n;{}]+\b' + re.escape(name)
|
||||
+ r'\([^;]*?\n\{.*?^\}', SOURCE, re.M | re.S))
|
||||
assert len(matches) == 1, f'expected exactly one production definition: {name}'
|
||||
return matches[0].group() + '\n'
|
||||
|
||||
|
||||
def declaration(text, name):
|
||||
match = re.search(r'^typedef (?:struct|enum) \{[^}]*\} ' + name + ';', text, re.M)
|
||||
assert match, name
|
||||
return match.group() + '\n'
|
||||
|
||||
|
||||
for setter, callback in (
|
||||
('wolfSSH_SetUserAuth', 'authenticate_user'),
|
||||
('wolfSSH_SetUserAuthTypes', 'allowed_auth_types'),
|
||||
('wolfSSH_SetUserAuthResult', 'authentication_result'),
|
||||
('wolfSSH_SetKeyboardAuthPrompts', 'reject_keyboard_auth'),
|
||||
):
|
||||
assert re.search(r'\b' + setter + r'\(context,\s*' + callback + r'\)',
|
||||
function('create_context')), setter
|
||||
for setter in ('wolfSSH_SetUserAuthCtx', 'wolfSSH_SetUserAuthResultCtx',
|
||||
'wolfSSH_SetKeyboardAuthCtx'):
|
||||
assert re.search(r'\b' + setter + r'\(slot->ssh,\s*slot\)',
|
||||
function('accept_connections')), setter
|
||||
assert re.search(r'^static ssh_auth_policy_t s_auth_policy;', SOURCE, re.M)
|
||||
|
||||
names = '''add_counter make_session_id allowed_auth_types clear_pending_principal
|
||||
close_authentication admit_authentication reject_keyboard_auth
|
||||
complete_authentication_attempt authenticate_password authenticate_public_key
|
||||
authenticate_user authentication_result wolfssh_would_block close_socket
|
||||
cleanup_slot request_slot_close start_runtime stop_runtime find_free_slot
|
||||
accept_connections flush_client_input receive_client_input flush_client_output
|
||||
flush_admin_input receive_admin_input ssh_transport_clear_counters'''.split()
|
||||
constants = '\n'.join(line for text in (HEADER, SOURCE) for line in text.splitlines()
|
||||
if line.startswith('#define SSH_TRANSPORT_')) + '\n'
|
||||
types = ''.join(declaration(HEADER, n) for n in (
|
||||
'ssh_transport_session_state_t', 'ssh_transport_session_route_t',
|
||||
'ssh_transport_counters_t')) + declaration(SOURCE, 'ssh_slot_t')
|
||||
fixture = (HERE / 'fixture.h').read_text()
|
||||
unit = fixture.replace('/* PRODUCTION TYPES */', constants + types)
|
||||
unit += '\n'.join(function(n) for n in names)
|
||||
unit += (HERE / 'test.c').read_text()
|
||||
env = dict(os.environ, CCACHE_DISABLE='1')
|
||||
with tempfile.TemporaryDirectory(prefix='ssh-auth-transport-') as directory:
|
||||
out = Path(directory)
|
||||
(out / 'test.c').write_text(unit)
|
||||
subprocess.run(shlex.split(os.environ.get('CC', 'cc')) + [
|
||||
'-std=c11', '-Wall', '-Wextra', '-Werror', '-g',
|
||||
'-I', str(ROOT / 'src'), str(out / 'test.c'),
|
||||
str(ROOT / 'src/ssh_auth_policy.c'), '-o', str(out / 'test')],
|
||||
env=env, check=True, timeout=30)
|
||||
subprocess.run([str(out / 'test')], env=env, check=True, timeout=10)
|
||||
print('PASS production callback registration and per-slot context source checks')
|
||||
@@ -0,0 +1,249 @@
|
||||
/* Included after verbatim production definitions by run.py. */
|
||||
static void zero_bytes(const void *p,size_t n) {
|
||||
const byte *b=p; for(size_t i=0;i<n;++i) assert(b[i]==0);
|
||||
}
|
||||
static ssh_slot_t *fresh(unsigned i) {
|
||||
assert(i<2); memset(&s_slots[i],0,sizeof(s_slots[i]));
|
||||
s_slots[i].state=SSH_TRANSPORT_SESSION_HANDSHAKE;
|
||||
s_slots[i].socket_fd=30+(int)i; s_slots[i].ssh=(WOLFSSH *)&s_lock;
|
||||
return &s_slots[i];
|
||||
}
|
||||
static void boot(void) {
|
||||
memset(s_slots,0,sizeof(s_slots)); memset(&s_counters,0,sizeof(s_counters));
|
||||
memset(&s_auth_policy,0,sizeof(s_auth_policy));
|
||||
s_context=NULL; s_listen_fd=-1; s_initialized=true; now_us=0;
|
||||
password_calls=key_calls=current_calls=shutdown_calls=close_calls=0;
|
||||
allocations=frees=context_frees=0; db_error=current_error=broker_error=ESP_OK;
|
||||
db_accept=db_current=true; allocation_fail=false;
|
||||
}
|
||||
static WS_UserAuthData password(void) {
|
||||
WS_UserAuthData a={.type=WOLFSSH_USERAUTH_PASSWORD,.username=(const byte *)"u",.usernameSz=1};
|
||||
a.sf.password.password=payload; a.sf.password.passwordSz=sizeof(payload); return a;
|
||||
}
|
||||
static WS_UserAuthData key(bool signed_key) {
|
||||
WS_UserAuthData a={.type=WOLFSSH_USERAUTH_PUBLICKEY,.username=(const byte *)"u",.usernameSz=1};
|
||||
a.sf.publicKey=(WS_UserAuthData_PublicKey){.hasSignature=signed_key,
|
||||
.publicKeyType=(const byte *)"k",.publicKeyTypeSz=1,
|
||||
.publicKey=payload,.publicKeySz=sizeof(payload)}; return a;
|
||||
}
|
||||
static int auth(ssh_slot_t *s,WS_UserAuthData *a) { return authenticate_user(a->type,a,s); }
|
||||
static void passwords(void) {
|
||||
for(unsigned mode=0;mode<4;++mode) {
|
||||
boot(); ssh_slot_t *s=fresh(0); WS_UserAuthData a=password();
|
||||
db_accept=mode==0; db_error=mode==2 ? ESP_FAIL : ESP_OK;
|
||||
a.sf.password.hasNewPassword=mode==3;
|
||||
const int expected[]={WOLFSSH_USERAUTH_SUCCESS,WOLFSSH_USERAUTH_INVALID_PASSWORD,
|
||||
WOLFSSH_USERAUTH_FAILURE,WOLFSSH_USERAUTH_INVALID_AUTHTYPE};
|
||||
assert(auth(s,&a)==expected[mode]);
|
||||
assert(password_calls==(mode==3 ? 0U : 1U));
|
||||
assert(s->authenticated==(mode==0) && s->principal_valid==(mode==0));
|
||||
assert(s->authentication_attempts==1 && s_counters.authentication_attempts==1);
|
||||
assert(s_counters.authentication_failures==(mode!=0));
|
||||
assert(s_counters.authentication_backend_errors==(mode==2));
|
||||
assert(s_counters.authentication_admissions==1);
|
||||
zero_bytes(&s->pending_principal,sizeof(s->pending_principal));
|
||||
}
|
||||
boot(); db_accept=false; WS_UserAuthData a=password();
|
||||
for(unsigned i=0;i<2;++i) {
|
||||
ssh_slot_t *s=fresh(i);
|
||||
for(unsigned j=0;j<3;++j) {
|
||||
assert(auth(s,&a)==(j==2 ? WOLFSSH_USERAUTH_REJECTED : WOLFSSH_USERAUTH_INVALID_PASSWORD));
|
||||
assert(s->close_requested==(j==2));
|
||||
}
|
||||
}
|
||||
assert(s_counters.authentication_limit_disconnects==2 && shutdown_calls==2);
|
||||
ssh_slot_t *s=fresh(0);
|
||||
assert(auth(s,&a)==WOLFSSH_USERAUTH_REJECTED && password_calls==6);
|
||||
assert(s->authentication_attempts==0 && s_counters.authentication_throttle_rejections==1);
|
||||
WS_UserAuthData signed_key=key(true); s=fresh(1);
|
||||
assert(auth(s,&signed_key)==WOLFSSH_USERAUTH_REJECTED && key_calls==0);
|
||||
assert(!s->awaiting_auth_result && !s->pending_principal_valid);
|
||||
assert(authentication_result(WOLFSSH_USERAUTH_SUCCESS,&signed_key,s)==WS_ERROR);
|
||||
assert(s_counters.authentication_attempts==6 && current_calls==0 && !s->authenticated);
|
||||
now_us=9999999; s=fresh(1); assert(auth(s,&a)==WOLFSSH_USERAUTH_REJECTED);
|
||||
now_us=10000000; s=fresh(0); assert(auth(s,&a)==WOLFSSH_USERAUTH_INVALID_PASSWORD);
|
||||
s=fresh(1); assert(auth(s,&a)==WOLFSSH_USERAUTH_REJECTED && password_calls==7);
|
||||
puts("PASS password outcomes, shared verification budget/refill, reconnect and three-failure closure");
|
||||
}
|
||||
static void signed_keys(void) {
|
||||
for(unsigned mode=0;mode<6;++mode) {
|
||||
boot(); ssh_slot_t *s=fresh(0); WS_UserAuthData a=key(true);
|
||||
db_accept=mode!=0; db_error=mode==1 ? ESP_FAIL : ESP_OK;
|
||||
int result=auth(s,&a); assert(key_calls==1);
|
||||
if(mode<2) {
|
||||
assert(result==(mode==0 ? WOLFSSH_USERAUTH_INVALID_PUBLICKEY : WOLFSSH_USERAUTH_FAILURE));
|
||||
assert(!s->awaiting_auth_result && !s->pending_principal_valid);
|
||||
assert(s_counters.authentication_attempts==1);
|
||||
/* Denied authorization never grants permission for signature work. */
|
||||
assert(authentication_result(WOLFSSH_USERAUTH_SUCCESS,&a,s)==WS_ERROR);
|
||||
} else {
|
||||
assert(result==WOLFSSH_USERAUTH_SUCCESS && s->awaiting_auth_result);
|
||||
assert(!s->authenticated && s_counters.authentication_attempts==0);
|
||||
db_current=mode!=3; current_error=mode==4 ? ESP_FAIL : ESP_OK;
|
||||
assert(authentication_result(mode==2 ? WOLFSSH_USERAUTH_FAILURE : WOLFSSH_USERAUTH_SUCCESS,
|
||||
&a,s)==((mode==3 || mode==4) ? WS_ERROR : WS_SUCCESS));
|
||||
assert(s->authenticated==(mode==5));
|
||||
assert(current_calls==(mode==2 ? 0U : 1U));
|
||||
assert(s_counters.authentication_attempts==1);
|
||||
assert(s_counters.authentication_admissions==1);
|
||||
assert(s_counters.authentication_failures==(mode!=5));
|
||||
assert(s_counters.authentication_backend_errors==(mode==4));
|
||||
bool before=s->authenticated;
|
||||
assert(authentication_result(WOLFSSH_USERAUTH_SUCCESS,&a,s)==WS_ERROR);
|
||||
assert(s->authenticated==before);
|
||||
}
|
||||
assert(s_counters.authentication_attempts==1 && s->close_requested);
|
||||
assert(!s->awaiting_auth_result && !s->pending_principal_valid);
|
||||
zero_bytes(&s->pending_principal,sizeof(s->pending_principal));
|
||||
}
|
||||
boot(); WS_UserAuthData a=key(true); ssh_slot_t *s=fresh(0);
|
||||
for(unsigned i=0;i<3;++i) {
|
||||
assert(auth(s,&a)==WOLFSSH_USERAUTH_SUCCESS);
|
||||
assert(authentication_result(WOLFSSH_USERAUTH_FAILURE,&a,s)==WS_SUCCESS);
|
||||
assert(s->close_requested==(i==2));
|
||||
}
|
||||
assert(s_counters.authentication_limit_disconnects==1);
|
||||
puts("PASS signed authorization, result/currentness failures, success, exactly-once completion and limits");
|
||||
}
|
||||
static void fences_and_probes(void) {
|
||||
boot(); WS_UserAuthData a=key(false);
|
||||
for(unsigned i=0;i<12;++i) {
|
||||
ssh_slot_t *s=fresh(i%2); db_accept=i%2==0;
|
||||
assert(auth(s,&a)==(db_accept ? WOLFSSH_USERAUTH_SUCCESS : WOLFSSH_USERAUTH_INVALID_PUBLICKEY));
|
||||
assert(!s->awaiting_auth_result && !s->authenticated && !s->authentication_attempts);
|
||||
}
|
||||
ssh_slot_t *s=fresh(0); assert(auth(s,&a)==WOLFSSH_USERAUTH_REJECTED);
|
||||
assert(key_calls==12 && s_counters.authentication_attempts==0);
|
||||
assert(s_counters.authentication_probe_admissions==12 && s_counters.authentication_probe_rejections==1);
|
||||
now_us=4999999; s=fresh(1); assert(auth(s,&a)==WOLFSSH_USERAUTH_REJECTED);
|
||||
now_us=5000000; db_accept=true; s=fresh(0); assert(auth(s,&a)==WOLFSSH_USERAUTH_SUCCESS);
|
||||
s=fresh(1); assert(auth(s,&a)==WOLFSSH_USERAUTH_REJECTED && key_calls==13);
|
||||
a=key(true); s=fresh(0); assert(auth(s,&a)==WOLFSSH_USERAUTH_SUCCESS);
|
||||
assert(s_counters.authentication_admissions==1); /* Probe pool independent. */
|
||||
for(unsigned mode=0;mode<7;++mode) {
|
||||
boot(); s=fresh(0); a=key(true);
|
||||
if(mode<3) assert(auth(s,&a)==WOLFSSH_USERAUTH_SUCCESS);
|
||||
if(mode==0) s->close_requested=true;
|
||||
if(mode==1) s->state=SSH_TRANSPORT_SESSION_CLOSING;
|
||||
if(mode==2) { assert(auth(s,&a)==WOLFSSH_USERAUTH_REJECTED); }
|
||||
if(mode==4) a.sf.publicKey.hasSignature=0;
|
||||
if(mode==5) a=password();
|
||||
assert(authentication_result(WOLFSSH_USERAUTH_SUCCESS,mode==6 ? NULL : &a,s)==WS_ERROR);
|
||||
assert(!s->authenticated && !s->principal_valid && !s->awaiting_auth_result);
|
||||
assert(s_counters.authentication_attempts==0 && current_calls==0);
|
||||
assert(auth(s,&a)==WOLFSSH_USERAUTH_REJECTED);
|
||||
}
|
||||
boot(); s=fresh(0); a=password();
|
||||
assert(authenticate_user(99,&a,s)==WOLFSSH_USERAUTH_REJECTED);
|
||||
assert(s_counters.authentication_method_rejections==1 && !password_calls);
|
||||
assert(allowed_auth_types(NULL,NULL)==(WOLFSSH_USERAUTH_PASSWORD|WOLFSSH_USERAUTH_PUBLICKEY));
|
||||
boot(); s=fresh(0); a=key(true); assert(auth(s,&a)==WOLFSSH_USERAUTH_SUCCESS);
|
||||
WS_UserAuthData_Keyboard keyboard; memset(&keyboard,0xa5,sizeof(keyboard));
|
||||
assert(reject_keyboard_auth(&keyboard,s)==WS_ERROR); zero_bytes(&keyboard,sizeof(keyboard));
|
||||
assert(s->close_requested && !s->pending_principal_valid && !s->awaiting_auth_result);
|
||||
assert(s_counters.authentication_method_rejections==1 && !s_counters.authentication_attempts);
|
||||
assert(reject_keyboard_auth(NULL,s)==WS_ERROR && s_counters.authentication_method_rejections==1);
|
||||
assert(reject_keyboard_auth(NULL,NULL)==WS_ERROR);
|
||||
puts("PASS bounded unsigned probes, independent pools, result/close fences and keyboard decline without prompts");
|
||||
}
|
||||
static void admissions_lifecycle(void) {
|
||||
boot(); assert(start_runtime()==ESP_OK);
|
||||
accepts_remaining=3; accept_connections();
|
||||
assert(allocations==2 && s_counters.capacity_rejections==1);
|
||||
assert(s_counters.handshake_admissions==2);
|
||||
assert(s_slots[0].generation==1 && s_slots[1].generation==1);
|
||||
assert(cleanup_slot(&s_slots[0]) && cleanup_slot(&s_slots[1]));
|
||||
allocation_fail=true; accepts_remaining=4; accept_connections();
|
||||
assert(allocations==6 && s_counters.handshake_admissions==6 && s_counters.handshake_failures==4);
|
||||
accepts_remaining=1; accept_connections();
|
||||
assert(allocations==6 && s_counters.handshake_throttle_rejections==1);
|
||||
WS_UserAuthData p=password(), k=key(false); db_accept=false;
|
||||
for(unsigned i=0;i<6;++i) assert(auth(fresh(i%2),&p)==WOLFSSH_USERAUTH_INVALID_PASSWORD);
|
||||
for(unsigned i=0;i<12;++i) assert(auth(fresh(i%2),&k)==WOLFSSH_USERAUTH_INVALID_PUBLICKEY);
|
||||
ssh_auth_policy_t saved=s_auth_policy;
|
||||
assert(stop_runtime()==ESP_OK && start_runtime()==ESP_OK);
|
||||
assert(memcmp(&saved,&s_auth_policy,sizeof(saved))==0);
|
||||
s_initialized=false; assert(ssh_transport_clear_counters()==ESP_ERR_INVALID_STATE);
|
||||
s_initialized=true; assert(ssh_transport_clear_counters()==ESP_OK);
|
||||
zero_bytes(&s_counters,sizeof(s_counters));
|
||||
assert(memcmp(&saved,&s_auth_policy,sizeof(saved))==0);
|
||||
accepts_remaining=1; accept_connections(); assert(allocations==6);
|
||||
assert(auth(fresh(0),&p)==WOLFSSH_USERAUTH_REJECTED);
|
||||
assert(auth(fresh(1),&k)==WOLFSSH_USERAUTH_REJECTED);
|
||||
assert(s_counters.handshake_throttle_rejections==1 && s_counters.authentication_throttle_rejections==1 && s_counters.authentication_probe_rejections==1);
|
||||
assert(cleanup_slot(&s_slots[0]) && cleanup_slot(&s_slots[1]));
|
||||
now_us=9999999; accepts_remaining=1; accept_connections(); assert(allocations==6);
|
||||
now_us=10000000; allocation_fail=false; accepts_remaining=2; accept_connections();
|
||||
assert(allocations==7 && s_counters.handshake_admissions==1);
|
||||
uint64_t count=UINT64_MAX-1; add_counter(&count,1); assert(count==UINT64_MAX);
|
||||
add_counter(&count,1); assert(count==UINT64_MAX); count=1;
|
||||
add_counter(&count,UINT64_MAX); assert(count==UINT64_MAX);
|
||||
s_counters.authentication_attempts=UINT64_MAX;
|
||||
s_counters.authentication_failures=UINT64_MAX;
|
||||
ssh_slot_t *s=fresh(1); s->authentication_attempts=UINT8_MAX;
|
||||
assert(!complete_authentication_attempt(s,true));
|
||||
assert(s->authentication_attempts==UINT8_MAX && s_counters.authentication_attempts==UINT64_MAX && s_counters.authentication_failures==UINT64_MAX);
|
||||
puts("PASS actual accept loop capacity/budget, failed allocations consume admission, restart/clear retain all pools, saturation");
|
||||
}
|
||||
static void buffers(void) {
|
||||
boot(); ssh_slot_t *s=fresh(0); s->route=SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE;
|
||||
memcpy(s->rx_buffer,payload,sizeof(payload)); s->rx_length=sizeof(payload);
|
||||
feed_ok=true; feed_consumed=2; assert(flush_admin_input(s,0));
|
||||
zero_bytes(s->rx_buffer,2); assert(s->rx_offset==2 && !memcmp(s->rx_buffer+2,payload+2,6));
|
||||
feed_ok=false; feed_consumed=2; assert(flush_admin_input(s,0));
|
||||
zero_bytes(s->rx_buffer,4); assert(s->rx_offset==4 && !memcmp(s->rx_buffer+4,payload+4,4));
|
||||
assert(s_counters.rx_accepted_bytes==4 && !s_counters.admin_console_input_rejections);
|
||||
feed_consumed=0; assert(flush_admin_input(s,0)); assert(s->rx_offset==4 && s_counters.admin_console_input_rejections==1);
|
||||
feed_ok=true; feed_consumed=4; assert(flush_admin_input(s,0));
|
||||
zero_bytes(s->rx_buffer,sizeof(payload)); assert(!s->rx_length && !s->rx_offset);
|
||||
const int retry[]={0,WS_WANT_READ,WS_WANT_WRITE,WS_REKEYING,WS_WINDOW_FULL,WS_CHAN_RXD};
|
||||
for(unsigned route=SSH_TRANSPORT_ROUTE_BROKER;route<=SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE;++route) {
|
||||
s->route=route; memcpy(s->tx_buffer,payload,sizeof(payload)); s->tx_length=sizeof(payload); s->tx_offset=0;
|
||||
for(size_t i=0;i<sizeof(retry)/sizeof(retry[0]);++i) {
|
||||
send_result=retry[i]; ssh_error=0; assert(flush_client_output(s));
|
||||
assert(!s->tx_offset && s->tx_length==sizeof(payload) && !memcmp(s->tx_buffer,payload,sizeof(payload)));
|
||||
}
|
||||
send_result=3; assert(flush_client_output(s)); assert(s->tx_offset==3);
|
||||
if(route==SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE) zero_bytes(s->tx_buffer,3);
|
||||
else assert(!memcmp(s->tx_buffer,payload,3));
|
||||
assert(!memcmp(s->tx_buffer+3,payload+3,5));
|
||||
send_result=WS_ERROR; ssh_error=WS_WANT_WRITE; assert(flush_client_output(s)); assert(s->tx_offset==3);
|
||||
ssh_error=WS_ERROR; assert(!flush_client_output(s)); assert(!memcmp(s->tx_buffer+3,payload+3,5));
|
||||
send_result=5; assert(flush_client_output(s)); assert(!s->tx_offset && !s->tx_length);
|
||||
if(route==SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE) zero_bytes(s->tx_buffer,sizeof(payload));
|
||||
else assert(!memcmp(s->tx_buffer,payload,sizeof(payload)));
|
||||
}
|
||||
for(size_t i=0;i<sizeof(retry)/sizeof(retry[0]);++i) {
|
||||
read_result=retry[i]; ssh_error=0; assert(receive_admin_input(s,0)); assert(!s->rx_length);
|
||||
assert(receive_client_input(s));
|
||||
}
|
||||
read_result=WS_ERROR; ssh_error=WS_ERROR; assert(!receive_admin_input(s,0)); assert(!receive_client_input(s));
|
||||
read_result=sizeof(payload); feed_consumed=3; feed_ok=false; assert(receive_admin_input(s,0));
|
||||
zero_bytes(s->rx_buffer,3); assert(s->rx_offset==3 && !memcmp(s->rx_buffer+3,payload+3,5));
|
||||
feed_consumed=5; assert(receive_admin_input(s,0)); zero_bytes(s->rx_buffer,sizeof(payload));
|
||||
s->route=SSH_TRANSPORT_ROUTE_BROKER; s->writer=true; broker_accepted=3;
|
||||
assert(receive_client_input(s)); assert(s->rx_offset==3 && !memcmp(s->rx_buffer,payload,sizeof(payload)));
|
||||
broker_error=ESP_ERR_TIMEOUT; broker_accepted=0; assert(flush_client_input(s)); assert(s->rx_offset==3);
|
||||
broker_error=ESP_OK; broker_accepted=5; assert(flush_client_input(s)); assert(!s->rx_length && !memcmp(s->rx_buffer,payload,sizeof(payload)));
|
||||
puts("PASS consumed admin RX including false+consumed, retry/partial TX wiping, binary serial buffers unchanged");
|
||||
}
|
||||
static void retirement(void) {
|
||||
boot(); ssh_slot_t *s=&s_slots[0]; memset(s,0xa5,sizeof(*s));
|
||||
s->generation=0x12345678; s->socket_fd=42; s->ssh=(WOLFSSH *)&s_lock;
|
||||
s->route=SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE; s->broker_client_id=7;
|
||||
broker_error=ESP_FAIL; assert(!cleanup_slot(s));
|
||||
assert(s->state==SSH_TRANSPORT_SESSION_CLOSING && s->generation==0x12345678);
|
||||
assert(s->socket_fd==-1 && s->ssh==NULL && frees==1);
|
||||
broker_error=ESP_OK; assert(cleanup_slot(s));
|
||||
assert(wipe_address==s && wipe_size==sizeof(*s));
|
||||
ssh_slot_t expected; memset(&expected,0,sizeof(expected));
|
||||
expected.state=SSH_TRANSPORT_SESSION_FREE; expected.generation=0x12345678; expected.socket_fd=-1;
|
||||
assert(!memcmp(s,&expected,sizeof(*s)) && frees==1 && close_calls==1);
|
||||
assert(cleanup_slot(s)); assert(!memcmp(s,&expected,sizeof(*s)));
|
||||
puts("PASS retirement whole-slot wipe, exact generation and fd=-1 sentinel, deferred broker cleanup");
|
||||
}
|
||||
int main(void) {
|
||||
passwords(); signed_keys(); fences_and_probes(); admissions_lifecycle(); buffers(); retirement();
|
||||
puts("PASS SSH transport extracted-production host suite");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user