Harden SSH Admission And Credential Input
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
#include <setjmp.h>
|
||||
typedef int esp_err_t;
|
||||
enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_STATE,
|
||||
ESP_ERR_NO_MEM, ESP_ERR_TIMEOUT, ESP_ERR_NOT_SUPPORTED, ESP_ERR_NOT_FOUND };
|
||||
ESP_ERR_NO_MEM, ESP_ERR_TIMEOUT, ESP_ERR_NOT_SUPPORTED, ESP_ERR_NOT_FOUND,
|
||||
ESP_ERR_INVALID_SIZE };
|
||||
enum { USER_ROLE_USER, USER_ROLE_ADMIN };
|
||||
#define USER_DATABASE_USERNAME_CAPACITY 16U
|
||||
typedef struct {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Hidden credential input regression
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
CCACHE_DISABLE=1 python3 tests/hidden_input/run.py
|
||||
```
|
||||
|
||||
The host harness exercises production UART0/shared remote prompt handling and the extracted password-confirmation helper with synthetic input. It covers maximum capacity, sticky overflow (including differing suffixes and later backspace), unsupported bytes, cancellation, read failure, remote disconnect/revocation, confirmation rejection, secret-free output, remote CRLF handling and unchanged visible editing.
|
||||
|
||||
A rejected hidden prompt must return an error with zero length and wiped output, never a truncated credential prefix. These tests do not change password policy or persistence contracts.
|
||||
|
||||
The UART fake does not reproduce driver flushing or delayed paired-CRLF timing. No device, real transport or hardware validation is implied. Those checks are deferred to the [combined Phase 9 target session](../../docs/security_hardening.md#combined-phase-9-target-validation--deferred-not-run).
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Actual UART/shared remote prompt readers with deterministic host IO/RTOS fakes."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IDF = Path(os.environ.get("IDF_PATH", str(Path.home() / ".platformio/packages/framework-espidf")))
|
||||
|
||||
def stripped(name):
|
||||
return "\n".join(line for line in (ROOT / name).read_text().splitlines()
|
||||
if not line.startswith(("#include", "#pragma once"))) + "\n"
|
||||
|
||||
user = (ROOT / "src/user_console.c").read_text()
|
||||
password = user[user.index("static esp_err_t read_password("):user.index("static void show_generated_password(")]
|
||||
unit = ((ROOT / "tests/admin_console_boundary/fakes.h").read_text()
|
||||
+ stripped("src/admin_ssh_console.h") + stripped("src/admin_ssh_console.c")
|
||||
+ (ROOT / "tests/hidden_input/uart_fakes.h").read_text()
|
||||
+ stripped("src/console_input.c")
|
||||
+ "\n#undef printf\n#undef putchar\n#undef fflush\n"
|
||||
+ "#define USER_DATABASE_PASSWORD_CAPACITY 64U\n"
|
||||
+ "#define USER_DATABASE_PASSWORD_MIN_LENGTH 12U\n"
|
||||
+ "#define ESP_ERR_INVALID_RESPONSE 100\n" + password
|
||||
+ (ROOT / "tests/hidden_input/test.c").read_text())
|
||||
with tempfile.TemporaryDirectory(prefix="hidden-input-") as directory:
|
||||
path = Path(directory)
|
||||
(path / "test.c").write_text(unit)
|
||||
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-g",
|
||||
str(path / "test.c"), str(IDF / "components/console/split_argv.c"),
|
||||
"-o", str(path / "test")], check=True, timeout=30)
|
||||
subprocess.run([str(path / "test")], check=True, timeout=10)
|
||||
@@ -0,0 +1,126 @@
|
||||
|
||||
static admin_ssh_console_token_t token={.session_id=7,.slot_generation=1};
|
||||
static user_principal_t admin={.role=USER_ROLE_ADMIN,.username="admin",.username_length=5};
|
||||
static bool live=true, disconnect_input, revoke_input, password_mode, visible;
|
||||
static esp_err_t expected;
|
||||
static uint8_t answer[65];
|
||||
static size_t answer_length;
|
||||
static unsigned calls;
|
||||
static bool owner_current(const admin_ssh_console_token_t *t, const user_principal_t *p)
|
||||
{ (void)t; (void)p; assert(!lock_depth); return live; }
|
||||
static bool drained(const admin_ssh_console_token_t *t)
|
||||
{ (void)t; assert(!lock_depth); return owner_drained; }
|
||||
static esp_err_t perform(const admin_ssh_console_token_t *t,
|
||||
admin_ssh_deferred_action_type_t action, uint32_t arg)
|
||||
{ (void)t; (void)action; (void)arg; ++actions; return ESP_ERR_NOT_SUPPORTED; }
|
||||
static const admin_console_owner_t owner={.is_current=owner_current,.drained=drained,.perform=perform};
|
||||
static void zeroed(const void *data, size_t n)
|
||||
{ const uint8_t *p=data; for(size_t i=0;i<n;++i) assert(p[i]==0); }
|
||||
static void reply(void)
|
||||
{
|
||||
++publications;
|
||||
/* Feed bytewise to exercise persistent state across transport packets. */
|
||||
while(input_offset<input_size) {
|
||||
uint8_t byte=input[input_offset++]; size_t consumed=0;
|
||||
assert(admin_ssh_console_feed_input(&token,&byte,1,&consumed) && consumed==1);
|
||||
if(s_sessions[0].prompt_state!=ADMIN_PROMPT_WAITING) break;
|
||||
}
|
||||
if(disconnect_input) admin_ssh_console_close(&token);
|
||||
if(revoke_input) live=false;
|
||||
}
|
||||
static void command(void)
|
||||
{
|
||||
++calls;
|
||||
memset(answer,0xa5,sizeof(answer)); answer_length=999;
|
||||
esp_err_t error=password_mode ? read_password(answer,&answer_length) :
|
||||
visible ? console_input_read_line("Input: ",answer,sizeof(answer),&answer_length) :
|
||||
console_input_read_hidden("Password: ",answer,sizeof(answer),12,64,&answer_length);
|
||||
assert(error==expected);
|
||||
if(error==ESP_OK) {
|
||||
assert(answer_length==64 && answer[64]==0);
|
||||
for(size_t i=0;i<64;++i) assert(answer[i]=='Q');
|
||||
} else {
|
||||
assert(!answer_length); zeroed(answer,sizeof(answer));
|
||||
}
|
||||
if(s_dispatch_remote) {
|
||||
zeroed(s_sessions[0].prompt_input,sizeof(s_sessions[0].prompt_input));
|
||||
assert(!s_sessions[0].prompt_rejected && !s_sessions[0].prompt_length);
|
||||
}
|
||||
}
|
||||
static void run_case(int route, const uint8_t *bytes, size_t n, esp_err_t result,
|
||||
bool passwords, bool disconnect, bool revoke, bool show)
|
||||
{
|
||||
input=bytes; input_size=n; input_offset=0; expected=result;
|
||||
password_mode=passwords; disconnect_input=disconnect; revoke_input=revoke; visible=show;
|
||||
publications=0; calls=0; live=true; uart_output_length=0; uart_output[0]=0;
|
||||
s_dispatch_remote=false;
|
||||
if(route==0) command();
|
||||
else {
|
||||
++token.slot_generation; token.transport=route==1 ? ADMIN_CONSOLE_TRANSPORT_SSH : ADMIN_CONSOLE_TRANSPORT_WEB;
|
||||
assert(admin_ssh_console_open_owned(&token,&admin,&owner)==ESP_OK);
|
||||
uint8_t out[4096]; size_t count;
|
||||
assert(admin_ssh_console_read_output(&token,out,sizeof(out),&count)==ESP_OK);
|
||||
size_t consumed;
|
||||
assert(admin_ssh_console_feed_input(&token,(const uint8_t *)"user password target\r",21,&consumed));
|
||||
assert(consumed==21);
|
||||
prompt_hook=reply; command_hook=command;
|
||||
if(!setjmp(loop_done)) worker_task(NULL);
|
||||
prompt_hook=NULL; command_hook=NULL;
|
||||
if(s_sessions[0].active) {
|
||||
assert(admin_ssh_console_read_output(&token,out,sizeof(out)-1,&count)==ESP_OK);
|
||||
out[count]=0;
|
||||
if(!show) assert(!strstr((char *)out,"QQQ"));
|
||||
admin_ssh_console_close(&token);
|
||||
}
|
||||
zeroed(&s_sessions[0],sizeof(s_sessions[0]));
|
||||
}
|
||||
assert(calls==1 && !lock_depth);
|
||||
assert(input_offset==n);
|
||||
if(!show) assert(!strstr(uart_output,"QQQ"));
|
||||
assert(publications==(passwords && n>=130 ? 2U : 1U));
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
assert(owner_drained && actions==0);
|
||||
assert(admin_ssh_console_init()==ESP_OK);
|
||||
assert(admin_ssh_console_start_uart_frontend()==ESP_OK);
|
||||
uint8_t bytes[140];
|
||||
for(int route=0;route<3;++route) {
|
||||
memset(bytes,'Q',64); bytes[64]='\r';
|
||||
run_case(route,bytes,65,ESP_OK,false,false,false,false);
|
||||
bytes[64]='\n'; run_case(route,bytes,65,ESP_OK,false,false,false,false);
|
||||
/* Confirmation accepts the exact maximum, not a truncated prefix. */
|
||||
memcpy(bytes+65,bytes,65);
|
||||
run_case(route,bytes,130,ESP_OK,true,false,false,false);
|
||||
if(route) {
|
||||
bytes[64]='\r'; bytes[65]='\n'; memset(bytes+66,'Q',64); bytes[130]='\r';
|
||||
run_case(route,bytes,131,ESP_OK,true,false,false,false);
|
||||
}
|
||||
for(int suffix='X';suffix<='Y';++suffix) {
|
||||
memset(bytes,'Q',64); bytes[64]=(uint8_t)suffix; bytes[65]='\r';
|
||||
run_case(route,bytes,66,ESP_ERR_INVALID_SIZE,true,false,false,false);
|
||||
bytes[65]=8; bytes[66]=127; bytes[67]='Q'; bytes[68]='\r';
|
||||
run_case(route,bytes,69,ESP_ERR_INVALID_SIZE,false,false,false,false);
|
||||
}
|
||||
/* Unsupported controls/high bytes cannot silently disappear, even if erased. */
|
||||
for(unsigned byte=0;byte<256;++byte) {
|
||||
if((byte>=32 && byte<=126) || byte==3 || byte==8 || byte==127 || byte==10 || byte==13) continue;
|
||||
memset(bytes,'Q',63); bytes[63]=(uint8_t)byte; bytes[64]=8;
|
||||
bytes[65]='Q'; bytes[66]='Q'; bytes[67]='\r';
|
||||
run_case(route,bytes,68,ESP_ERR_INVALID_SIZE,false,false,false,false);
|
||||
}
|
||||
memset(bytes,'Q',64); bytes[64]=8; bytes[65]='Q'; bytes[66]=127; bytes[67]='Q'; bytes[68]='\r';
|
||||
run_case(route,bytes,69,ESP_OK,false,false,false,false);
|
||||
memset(bytes,'Q',65); bytes[65]=3;
|
||||
run_case(route,bytes,66,ESP_ERR_INVALID_STATE,false,false,false,false);
|
||||
run_case(route,bytes,65,route ? ESP_ERR_NOT_FOUND : ESP_FAIL,false,route!=0,false,false);
|
||||
if(route) run_case(route,bytes,65,ESP_ERR_NOT_FOUND,false,false,true,false);
|
||||
/* Visible prompts retain their existing truncation/edit behavior. */
|
||||
memset(bytes,'Q',65); bytes[65]=8; bytes[66]='Q'; bytes[67]='\r';
|
||||
run_case(route,bytes,68,ESP_OK,false,false,false,true);
|
||||
/* A failed confirmation also wipes the first full password. */
|
||||
memset(bytes,'Q',64); bytes[64]='\r'; memset(bytes+65,'Q',65); bytes[130]='\r';
|
||||
run_case(route,bytes,131,ESP_ERR_INVALID_SIZE,true,false,false,false);
|
||||
}
|
||||
puts("PASS: UART0/SSH/web exact capacity, sticky overflow/suffix/backspace, all unsupported bytes, cancellation/IO failure/disconnect/revocation, no echo, confirmation and visible editing");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
#include <stdarg.h>
|
||||
#define UART_NUM_0 0
|
||||
static const uint8_t *input;
|
||||
static size_t input_size, input_offset;
|
||||
static char uart_output[4096];
|
||||
static size_t uart_output_length;
|
||||
static unsigned publications;
|
||||
static esp_err_t uart_flush_input(int uart) { assert(uart==0); ++publications; return ESP_OK; }
|
||||
static int uart_read_bytes(int uart, void *out, size_t n, unsigned wait)
|
||||
{
|
||||
assert(uart==0 && n==1 && wait==portMAX_DELAY && !lock_depth);
|
||||
if (input_offset==input_size) return -1;
|
||||
*(uint8_t *)out=input[input_offset++];
|
||||
return 1;
|
||||
}
|
||||
static int capture_printf(const char *format, ...)
|
||||
{
|
||||
va_list ap; va_start(ap,format);
|
||||
int n=vsnprintf(uart_output+uart_output_length,
|
||||
sizeof(uart_output)-uart_output_length,format,ap);
|
||||
va_end(ap); assert(n>=0 && (size_t)n<sizeof(uart_output)-uart_output_length);
|
||||
uart_output_length+=(size_t)n; return n;
|
||||
}
|
||||
static int capture_putchar(int c) { return capture_printf("%c",c); }
|
||||
static int capture_fflush(FILE *f) { (void)f; return 0; }
|
||||
#define printf capture_printf
|
||||
#define putchar capture_putchar
|
||||
#define fflush capture_fflush
|
||||
@@ -0,0 +1,54 @@
|
||||
# SSH authentication admission policy host tests
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
CCACHE_DISABLE=1 python3 tests/ssh_auth_policy/run.py
|
||||
CCACHE_DISABLE=1 CFLAGS='-O1 -g -fsanitize=undefined -fno-sanitize-recover=all' python3 tests/ssh_auth_policy/run.py
|
||||
```
|
||||
|
||||
Requires Python 3 and a host C11 compiler (`cc`, or `CC`). `CFLAGS` may override
|
||||
optimization/add sanitizers. The runner disables ccache, compiles the actual
|
||||
`src/ssh_auth_policy.c` with strict warnings, and runs in a temporary directory.
|
||||
No ESP-IDF, mocks, third-party dependencies, sleeps or real clock are involved.
|
||||
|
||||
## Contract
|
||||
|
||||
`ssh_auth_policy_t` is zero-initialized, allocation-free, single-owner state
|
||||
(72 bytes on the tested host, compile-time limit of 72 bytes). Three independent
|
||||
buckets lazily start at capacity:
|
||||
|
||||
| Kind | Capacity | Refill |
|
||||
| --- | --- | --- |
|
||||
| `SSH_AUTH_POLICY_HANDSHAKE` | 6 | 1 token / 10 seconds |
|
||||
| `SSH_AUTH_POLICY_VERIFICATION` | 6 | 1 token / 10 seconds |
|
||||
| `SSH_AUTH_POLICY_PROBE` | 12 | 1 token / 5 seconds |
|
||||
|
||||
The header exposes each capacity and refill interval in microseconds.
|
||||
`ssh_auth_policy_admit(policy, kind, now_us)` consumes one token on success,
|
||||
with no refund for subsequent failure. Below capacity, fractional elapsed credit
|
||||
is retained. Reaching capacity discards all surplus, including fractional credit.
|
||||
Empty-bucket denials do not shift the refill deadline or incur debt.
|
||||
|
||||
Time must be nonnegative and nondecreasing across the shared policy, including
|
||||
across classes and ordinary rate-limit denials. Equal timestamps are valid.
|
||||
Negative/regressing time, invalid enum values and NULL fail closed without
|
||||
mutation. Refill arithmetic remains bounded through `INT64_MAX`.
|
||||
|
||||
The integrating SSH owner must retain ONE static policy across callers/sessions,
|
||||
SSH stop/start, and counter clears; only reboot zeroes it. There are no locks,
|
||||
timers, clock reads, sleeps, reset or refund APIs. These tests do not integrate
|
||||
`ssh_transport` or CMake, and do not validate lifecycle wiring.
|
||||
|
||||
## Coverage
|
||||
|
||||
All three classes: initial burst at zero and `INT64_MAX`; refill boundaries at
|
||||
minus/exact/plus one microsecond; partial and multi-token credit; 998 consecutive
|
||||
exact-rate refill cycles with intervening denials; idle saturation with no
|
||||
fractional surplus; huge forward jumps including zero to `INT64_MAX`; negative
|
||||
and regressing time with byte-for-byte unchanged state; regression after an
|
||||
initial zero timestamp and after denial. Also checks invalid enum/NULL handling,
|
||||
independent class budgets, cross-class monotonic validation, and two logical
|
||||
callers sharing one exhausted budget.
|
||||
|
||||
Hardware validation is deferred to the combined Phase 9 validation.
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile and run the actual allocation-free firmware policy on the host."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
env = dict(os.environ, CCACHE_DISABLE="1")
|
||||
with tempfile.TemporaryDirectory(prefix="ssh-auth-policy-") as temporary:
|
||||
binary = Path(temporary) / "test"
|
||||
command = shlex.split(env.get("CC", "cc")) + [
|
||||
"-std=c11", "-Wall", "-Wextra", "-Werror", "-pedantic",
|
||||
*shlex.split(env.get("CFLAGS", """-O2""")),
|
||||
"-I", str(ROOT / "src"),
|
||||
str(ROOT / "src/ssh_auth_policy.c"),
|
||||
str(ROOT / "tests/ssh_auth_policy/test.c"),
|
||||
"-o", str(binary),
|
||||
]
|
||||
subprocess.run(command, env=env, check=True)
|
||||
subprocess.run([str(binary)], env=env, check=True)
|
||||
@@ -0,0 +1,147 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include "ssh_auth_policy.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CHECK(condition) do { \
|
||||
if (!(condition)) { \
|
||||
fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #condition); \
|
||||
exit(EXIT_FAILURE); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
_Static_assert(sizeof(ssh_auth_policy_t) <= 72, "policy must remain small");
|
||||
_Static_assert(SSH_AUTH_POLICY_HANDSHAKE_CAPACITY == 6, "handshake burst");
|
||||
_Static_assert(SSH_AUTH_POLICY_VERIFICATION_CAPACITY == 6, "verification burst");
|
||||
_Static_assert(SSH_AUTH_POLICY_PROBE_CAPACITY == 12, "probe burst");
|
||||
_Static_assert(SSH_AUTH_POLICY_HANDSHAKE_REFILL_US == 10000000, "handshake rate");
|
||||
_Static_assert(SSH_AUTH_POLICY_VERIFICATION_REFILL_US == 10000000, "verification rate");
|
||||
_Static_assert(SSH_AUTH_POLICY_PROBE_REFILL_US == 5000000, "probe rate");
|
||||
|
||||
static void drain(ssh_auth_policy_t *p, ssh_auth_policy_kind_t kind,
|
||||
int64_t now, unsigned count)
|
||||
{
|
||||
for (unsigned i = 0; i < count; ++i) {
|
||||
CHECK(ssh_auth_policy_admit(p, kind, now));
|
||||
}
|
||||
CHECK(!ssh_auth_policy_admit(p, kind, now));
|
||||
}
|
||||
|
||||
static void unchanged(ssh_auth_policy_t *p, ssh_auth_policy_kind_t kind, int64_t now)
|
||||
{
|
||||
unsigned char before[sizeof(*p)];
|
||||
memcpy(before, p, sizeof(*p));
|
||||
CHECK(!ssh_auth_policy_admit(p, kind, now));
|
||||
CHECK(memcmp(before, p, sizeof(*p)) == 0);
|
||||
}
|
||||
|
||||
static void test_class(ssh_auth_policy_kind_t kind, unsigned capacity, int64_t interval)
|
||||
{
|
||||
ssh_auth_policy_t p = {0};
|
||||
drain(&p, kind, 0, capacity);
|
||||
unchanged(&p, kind, -1); /* Initial zero is a real timestamp. */
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, 1));
|
||||
unchanged(&p, kind, 0); /* Regression after an empty-bucket denial. */
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, interval - 1));
|
||||
unchanged(&p, kind, interval - 2);
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, interval));
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, interval + 1));
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, 2 * interval));
|
||||
|
||||
/* Many denials cannot extend cooldown; exactly one token each interval. */
|
||||
for (int64_t n = 3; n <= 1000; ++n) {
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, n * interval - 1));
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, n * interval));
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, n * interval));
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, n * interval + 1));
|
||||
}
|
||||
|
||||
p = (ssh_auth_policy_t){0};
|
||||
drain(&p, kind, 0, capacity);
|
||||
/* Refill multiple tokens without losing fractional elapsed credit. */
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, 2 * interval + interval / 2));
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, 3 * interval - 1));
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, 3 * interval - 1));
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, 3 * interval));
|
||||
|
||||
p = (ssh_auth_policy_t){0};
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, 0));
|
||||
const int64_t idle = 100 * interval + interval / 2;
|
||||
drain(&p, kind, idle, capacity); /* Full idle discards fractional surplus. */
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, idle + interval - 1));
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, idle + interval));
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, idle + interval + 1));
|
||||
|
||||
p = (ssh_auth_policy_t){0};
|
||||
drain(&p, kind, 0, capacity);
|
||||
const int64_t late = INT64_MAX - interval;
|
||||
drain(&p, kind, late, capacity); /* Huge forward jump saturates, not wraps. */
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, INT64_MAX - 1));
|
||||
CHECK(ssh_auth_policy_admit(&p, kind, INT64_MAX));
|
||||
CHECK(!ssh_auth_policy_admit(&p, kind, INT64_MAX));
|
||||
unchanged(&p, kind, INT64_MAX - 1);
|
||||
|
||||
p = (ssh_auth_policy_t){0};
|
||||
drain(&p, kind, INT64_MAX, capacity); /* Lazy init at maximum timestamp. */
|
||||
p = (ssh_auth_policy_t){0};
|
||||
drain(&p, kind, 0, capacity);
|
||||
drain(&p, kind, INT64_MAX, capacity); /* Direct maximum-sized subtraction. */
|
||||
}
|
||||
|
||||
static bool caller_a(ssh_auth_policy_t *p)
|
||||
{
|
||||
return ssh_auth_policy_admit(p, SSH_AUTH_POLICY_HANDSHAKE, 0);
|
||||
}
|
||||
|
||||
static bool caller_b(ssh_auth_policy_t *p)
|
||||
{
|
||||
return ssh_auth_policy_admit(p, SSH_AUTH_POLICY_HANDSHAKE, 0);
|
||||
}
|
||||
|
||||
static void test_validation_and_sharing(void)
|
||||
{
|
||||
ssh_auth_policy_t p = {0};
|
||||
CHECK(!ssh_auth_policy_admit(NULL, SSH_AUTH_POLICY_HANDSHAKE, 0));
|
||||
unchanged(&p, SSH_AUTH_POLICY_HANDSHAKE, INT64_MIN);
|
||||
unchanged(&p, (ssh_auth_policy_kind_t)-1, 0);
|
||||
unchanged(&p, SSH_AUTH_POLICY_KIND_COUNT, 0);
|
||||
unchanged(&p, (ssh_auth_policy_kind_t)INT_MAX, INT64_MAX);
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
CHECK(caller_a(&p));
|
||||
CHECK(caller_b(&p));
|
||||
}
|
||||
CHECK(!caller_a(&p));
|
||||
CHECK(!caller_b(&p));
|
||||
drain(&p, SSH_AUTH_POLICY_VERIFICATION, 0, 6);
|
||||
drain(&p, SSH_AUTH_POLICY_PROBE, 0, 12);
|
||||
CHECK(ssh_auth_policy_admit(&p, SSH_AUTH_POLICY_PROBE, 5000000));
|
||||
CHECK(!ssh_auth_policy_admit(&p, SSH_AUTH_POLICY_HANDSHAKE, 5000000));
|
||||
CHECK(!ssh_auth_policy_admit(&p, SSH_AUTH_POLICY_VERIFICATION, 5000000));
|
||||
CHECK(ssh_auth_policy_admit(&p, SSH_AUTH_POLICY_HANDSHAKE, 10000000));
|
||||
CHECK(ssh_auth_policy_admit(&p, SSH_AUTH_POLICY_VERIFICATION, 10000000));
|
||||
CHECK(ssh_auth_policy_admit(&p, SSH_AUTH_POLICY_PROBE, 10000000));
|
||||
unchanged(&p, (ssh_auth_policy_kind_t)-1, INT64_MAX);
|
||||
unchanged(&p, SSH_AUTH_POLICY_PROBE, -1);
|
||||
unchanged(&p, SSH_AUTH_POLICY_VERIFICATION, 9999999);
|
||||
|
||||
p = (ssh_auth_policy_t){0};
|
||||
CHECK(ssh_auth_policy_admit(&p, SSH_AUTH_POLICY_HANDSHAKE, 100));
|
||||
unchanged(&p, SSH_AUTH_POLICY_PROBE, 99); /* Even an uninitialized class. */
|
||||
drain(&p, SSH_AUTH_POLICY_PROBE, 100, 12);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_class(SSH_AUTH_POLICY_HANDSHAKE, SSH_AUTH_POLICY_HANDSHAKE_CAPACITY,
|
||||
SSH_AUTH_POLICY_HANDSHAKE_REFILL_US);
|
||||
test_class(SSH_AUTH_POLICY_VERIFICATION, SSH_AUTH_POLICY_VERIFICATION_CAPACITY,
|
||||
SSH_AUTH_POLICY_VERIFICATION_REFILL_US);
|
||||
test_class(SSH_AUTH_POLICY_PROBE, SSH_AUTH_POLICY_PROBE_CAPACITY,
|
||||
SSH_AUTH_POLICY_PROBE_REFILL_US);
|
||||
test_validation_and_sharing();
|
||||
printf("ssh_auth_policy: all tests passed (policy size: %zu bytes)\n", sizeof(ssh_auth_policy_t));
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
# Pinned wolfSSH authentication control-flow contract
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py
|
||||
```
|
||||
|
||||
Requires Python 3, a host C compiler (`CC`, default `cc`), installed managed
|
||||
wolfSSH, and an existing firmware compilation database/toolchain. No packages
|
||||
are downloaded and no firmware build or device commands run. Generated C and
|
||||
the executable live in a temporary directory and are removed on exit. Compile,
|
||||
preprocess and execution subprocesses have 30/30/10-second limits.
|
||||
|
||||
The runner prefers the sole `.pio/build/*/compile_commands.json`, otherwise the
|
||||
root database. Select another existing database with `--compile-commands PATH`.
|
||||
It preprocesses the actual wolfSSH `internal.c` compile command (`-E -dM`) and
|
||||
checks this reviewed profile:
|
||||
|
||||
- `LIBWOLFSSH_VERSION_HEX == 0x01004020` (1.4.20).
|
||||
- RSA disabled; ECDSA and Ed25519 not disabled.
|
||||
- Certificates, `none` authentication and `NO_FAILURE_ON_REJECTED` not defined.
|
||||
|
||||
For hosts without the ESP compiler/database, explicitly use `--host-only`.
|
||||
This prints a **SKIP** for production feature verification; it still checks the
|
||||
source/version/pin and executes the host contract with the reviewed feature
|
||||
profile. A stale compilation database is not proof of the next firmware build's
|
||||
configuration.
|
||||
|
||||
## What executes
|
||||
|
||||
`run.py` checks the exact application wolfSSH pin, installed version header and
|
||||
reviewed SHA-256 of `managed_components/wolfssl__wolfssh/src/internal.c`:
|
||||
|
||||
```text
|
||||
81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9
|
||||
```
|
||||
|
||||
Any same-version source change fails before compilation. **Re-audit before
|
||||
updating this hash**; do not automatically bless a dependency update.
|
||||
|
||||
The runner extracts actual function definitions by balancing braces after
|
||||
masking comments/string literals. It does not rewrite their bodies:
|
||||
|
||||
- `GetBoolean`, `GetUint32`, `GetSize`, `GetStringRef`
|
||||
- `DoUserAuthRequestPassword`, `DoUserAuthRequestPublicKey`, `DoUserAuthRequest`
|
||||
- `SendUserAuthKeyboardRequest`, `GetAllowedAuth`
|
||||
- `SendChannelData`
|
||||
|
||||
Callback data structures, auth result constants and method masks are extracted
|
||||
from the installed public header. `contract.c` supplies small session/context
|
||||
models, name/algorithm lookup, crypto and packet-output doubles. Binary request
|
||||
fixtures execute the extracted parsers; ordered event traces assert callback,
|
||||
hashing/signature and response order, rather than inspecting source substrings.
|
||||
|
||||
The 35 cases cover:
|
||||
|
||||
- Ed25519 and ECDSA: signed authorization rejection (`INVALID_PUBLICKEY`,
|
||||
`FAILURE`, `REJECTED`, `INVALID_USER`, `INVALID_AUTHTYPE`) never hashes,
|
||||
verifies or calls the result callback.
|
||||
- Both unsigned probe outcomes: no signature work/result callback; an accepted
|
||||
probe sends PK_OK but does not complete authentication.
|
||||
- Bad signatures, good signatures, success-result veto, ignored failure-result
|
||||
callback return, and auth `WOULD_BLOCK`.
|
||||
- Password success/failure, rejected password change, and the installed parser's
|
||||
callback on a truncated new-password-length field. No password result callback.
|
||||
- Disabled `none`, unknown methods/key algorithms and truncated signed framing.
|
||||
- Direct keyboard-interactive dispatch invokes a **registered non-NULL rejecting
|
||||
prompt callback**, returns error and purges without preparing/building/sending
|
||||
a prompt. The actual library still writes the message-ID byte into its existing
|
||||
output buffer on this path; the test models that buffer and checks this detail.
|
||||
- The actual advertised-method builder excludes keyboard despite the registered
|
||||
keyboard callback, because the allowed-types callback overrides the defaults.
|
||||
- Actual `SendChannelData` copies the bounded consumed prefix before returning a
|
||||
positive count, both on send success and `WS_WANT_WRITE`. Wiping that caller
|
||||
prefix leaves the library copy intact. A blocked flush of earlier data returns
|
||||
a negative code without consuming new data.
|
||||
|
||||
## Limits / ownership
|
||||
|
||||
This is a library parser/control-flow regression, **not application callback
|
||||
integration coverage**. Its rejecting keyboard callback models the parent's
|
||||
registration and return policy; it does not prove production registration,
|
||||
admission counters, awaiting-result state, principal promotion, or admin wiping.
|
||||
Those belong to the separate application unit suite.
|
||||
|
||||
Crypto helpers are instrumented doubles; algorithm name lookup is limited to
|
||||
fixture names. Packet construction/network send, hashing and session internals
|
||||
are modeled. This does not validate cryptographic correctness, encrypted packet
|
||||
decoding, real sockets, asynchronous re-entry, complete malformed-input safety,
|
||||
allocation failure, memory erasure throughout wolfSSH, or device behavior. The
|
||||
send test proves only the extracted copy/consumed control flow with successful
|
||||
packet preparation/bundling and the specified send outcomes—not the entire
|
||||
admin transmit loop or TLS/SSH buffer lifecycle.
|
||||
|
||||
All Phase9 hardware validation remains deferred to the combined phase.
|
||||
@@ -0,0 +1,246 @@
|
||||
/* Control-flow doubles only: no cryptographic implementation or real credentials. */
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef uint8_t byte;
|
||||
typedef uint32_t word32;
|
||||
#include "auth_types.h"
|
||||
|
||||
/* Reviewed production feature profile. Never enable certificates/none silently. */
|
||||
#define WOLFSSH_NO_RSA
|
||||
#if defined(WOLFSSH_CERTS) || defined(WOLFSSH_ALLOW_USERAUTH_NONE) || \
|
||||
defined(WOLFSSH_NO_ECDSA) || defined(WOLFSSH_NO_ED25519) || \
|
||||
defined(NO_FAILURE_ON_REJECTED)
|
||||
#error "Unexpected wolfSSH auth-contract feature profile"
|
||||
#endif
|
||||
#define WLOG(...) ((void)0)
|
||||
#define WMEMSET memset
|
||||
#define WMEMCPY memcpy
|
||||
#define WSTRNCAT strncat
|
||||
#define XSTRLEN strlen
|
||||
#define BOOLEAN_SZ 1
|
||||
#define UINT32_SZ 4
|
||||
#define LENGTH_SZ 4
|
||||
#define MSG_ID_SZ 1
|
||||
#define MAX_AUTH_STRING 80
|
||||
#define WOLFSSH_MAX_PROMPTS 8
|
||||
#define WC_MAX_DIGEST_SIZE 64
|
||||
#define min(a,b) ((a) < (b) ? (a) : (b))
|
||||
enum { WS_SUCCESS = 0, WS_ERROR = -1, WS_BUFFER_E = -2,
|
||||
WS_BAD_ARGUMENT = -3, WS_USER_AUTH_E = -4, WS_AUTH_PENDING = -5,
|
||||
WS_INVALID_ALGO_ID = -6, WS_CRYPTO_FAILED = -7, WS_BAD_USAGE = -8,
|
||||
WS_WANT_WRITE = -9, WS_REKEYING = -10, WS_INVALID_CHANID = -11,
|
||||
WS_WINDOW_FULL = -12 };
|
||||
|
||||
enum { ID_NONE, ID_UNKNOWN, ID_USERAUTH_PASSWORD, ID_USERAUTH_KEYBOARD,
|
||||
ID_USERAUTH_PUBLICKEY, ID_ED25519, ID_ECDSA_SHA2_NISTP256,
|
||||
ID_ECDSA_SHA2_NISTP384, ID_ECDSA_SHA2_NISTP521 };
|
||||
enum { WOLFSSH_ENDPOINT_SERVER, CLIENT_USERAUTH_DONE = 20,
|
||||
MSGID_USERAUTH_REQUEST, MSGID_USERAUTH_INFO_REQUEST, MSGID_CHANNEL_DATA,
|
||||
WS_CHANNEL_ID_SELF };
|
||||
enum wc_HashType { WC_HASH_TYPE_SHA };
|
||||
typedef int wc_HashAlg;
|
||||
typedef struct WOLFSSH WOLFSSH;
|
||||
typedef struct {
|
||||
int (*userAuthCb)(byte, WS_UserAuthData *, void *);
|
||||
int (*userAuthResultCb)(byte, WS_UserAuthData *, void *);
|
||||
int (*keyboardAuthCb)(WS_UserAuthData_Keyboard *, void *);
|
||||
int (*userAuthTypesCb)(WOLFSSH *, void *);
|
||||
} WOLFSSH_CTX;
|
||||
struct WOLFSSH {
|
||||
WOLFSSH_CTX *ctx;
|
||||
void *userAuthCtx, *userAuthResultCtx, *keyboardAuthCtx;
|
||||
int clientState, isKeying, error;
|
||||
byte sessionId[32];
|
||||
word32 sessionIdSz;
|
||||
struct { byte *buffer; word32 length, plainSz; } outputBuffer;
|
||||
struct { word32 promptCount; } kbAuth;
|
||||
};
|
||||
typedef struct {
|
||||
word32 peerWindowSz, peerMaxPacketSz, maxPacketSz, peerChannel;
|
||||
} WOLFSSH_CHANNEL;
|
||||
static WOLFSSH_CHANNEL channel;
|
||||
static const byte cannedKeyAlgoClient[] = { ID_ED25519, ID_ECDSA_SHA2_NISTP256 };
|
||||
static const word32 cannedKeyAlgoClientSz = sizeof(cannedKeyAlgoClient);
|
||||
static char events[64];
|
||||
static unsigned event_count, groups;
|
||||
static int auth_return, crypto_return, result_return, send_return;
|
||||
static int expect_new_password;
|
||||
static void event(char c) { assert(event_count + 1 < sizeof(events)); events[event_count++] = c; }
|
||||
static void ato32(const byte *b, word32 *v) {
|
||||
*v = (word32)b[0] << 24 | (word32)b[1] << 16 | (word32)b[2] << 8 | b[3];
|
||||
}
|
||||
static void c32toa(word32 v, byte *b) {
|
||||
b[0] = v >> 24; b[1] = v >> 16; b[2] = v >> 8; b[3] = v;
|
||||
}
|
||||
static byte NameToId(const char *s, word32 n) {
|
||||
static const struct { const char *s; byte id; } names[] = {
|
||||
{"none", ID_NONE}, {"password", ID_USERAUTH_PASSWORD},
|
||||
{"keyboard-interactive", ID_USERAUTH_KEYBOARD}, {"publickey", ID_USERAUTH_PUBLICKEY},
|
||||
{"ssh-ed25519", ID_ED25519}, {"ecdsa-sha2-nistp256", ID_ECDSA_SHA2_NISTP256}
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof(names)/sizeof(names[0]); ++i)
|
||||
if (strlen(names[i].s) == n && !memcmp(s, names[i].s, n)) return names[i].id;
|
||||
return ID_UNKNOWN;
|
||||
}
|
||||
static byte MatchIdLists(int side, const byte *id, word32 count, const byte *list, word32 n) {
|
||||
for (word32 i = 0; i < n; ++i) if (*id == list[i]) return *id;
|
||||
return ID_UNKNOWN;
|
||||
}
|
||||
static int wolfSSH_SetUsernameRaw(WOLFSSH *s, const byte *u, word32 n) { return WS_SUCCESS; }
|
||||
static int SendUserAuthFailure(WOLFSSH *s, byte partial) { event('F'); return WS_SUCCESS; }
|
||||
static int SendUserAuthPkOk(WOLFSSH *s, const byte *a, word32 an, const byte *k, word32 kn) {
|
||||
event('P'); return WS_SUCCESS;
|
||||
}
|
||||
static int DoUserAuthRequestEd25519(WOLFSSH *s, WS_UserAuthData_PublicKey *p, WS_UserAuthData *a) {
|
||||
event('C'); return crypto_return;
|
||||
}
|
||||
static int DoUserAuthRequestEcc(WOLFSSH *s, WS_UserAuthData_PublicKey *p,
|
||||
enum wc_HashType h, byte *d, word32 n) {
|
||||
event('C'); return crypto_return;
|
||||
}
|
||||
static enum wc_HashType HashForId(byte id) { return WC_HASH_TYPE_SHA; }
|
||||
static int wc_HashGetDigestSize(enum wc_HashType h) { return 32; }
|
||||
static int wc_HashInit(wc_HashAlg *h, enum wc_HashType id) { event('H'); return 0; }
|
||||
static int HashUpdate(wc_HashAlg *h, enum wc_HashType id, const byte *b, word32 n) { return 0; }
|
||||
static int wc_HashFinal(wc_HashAlg *h, enum wc_HashType id, byte *b) { return 0; }
|
||||
static void wc_HashFree(wc_HashAlg *h, enum wc_HashType id) {}
|
||||
static int PrepareUserAuthRequestKeyboard(WOLFSSH *s, word32 *n, WS_UserAuthData *a) {
|
||||
event('Q'); return WS_SUCCESS;
|
||||
}
|
||||
static int BuildUserAuthRequestKeyboard(WOLFSSH *s, byte *b, word32 *n, WS_UserAuthData *a) {
|
||||
event('B'); return WS_SUCCESS;
|
||||
}
|
||||
static int PreparePacket(WOLFSSH *s, word32 n) { event('T'); return WS_SUCCESS; }
|
||||
static int BundlePacket(WOLFSSH *s) { event('B'); return WS_SUCCESS; }
|
||||
static int wolfSSH_SendPacket(WOLFSSH *s) { event('S'); s->error = send_return; return send_return; }
|
||||
static void PurgePacket(WOLFSSH *s) { event('X'); }
|
||||
static WOLFSSH_CHANNEL *ChannelFind(WOLFSSH *s, word32 id, int kind) { return &channel; }
|
||||
|
||||
#include "actual.c"
|
||||
|
||||
static int authorize(byte method, WS_UserAuthData *a, void *ctx) {
|
||||
event('A');
|
||||
assert(method == a->type);
|
||||
assert(a->usernameSz == 4 && !memcmp(a->username, "test", 4));
|
||||
if (method == WOLFSSH_USERAUTH_PASSWORD) {
|
||||
assert(a->sf.password.hasNewPassword == expect_new_password);
|
||||
assert(a->sf.password.passwordSz == 5);
|
||||
assert(!memcmp(a->sf.password.password, "dummy", 5));
|
||||
}
|
||||
return auth_return;
|
||||
}
|
||||
static int result(byte outcome, WS_UserAuthData *a, void *ctx) {
|
||||
assert(a->type == WOLFSSH_USERAUTH_PUBLICKEY && a->sf.publicKey.hasSignature);
|
||||
event(outcome == WOLFSSH_USERAUTH_SUCCESS ? 'R' : 'r');
|
||||
return result_return;
|
||||
}
|
||||
/* Models the parent's registered rejecting prompt callback, not app accounting. */
|
||||
static int reject_keyboard(WS_UserAuthData_Keyboard *k, void *ctx) {
|
||||
event('K'); memset(k, 0, sizeof(*k)); return WS_ERROR;
|
||||
}
|
||||
static int allowed(WOLFSSH *s, void *ctx) {
|
||||
return WOLFSSH_USERAUTH_PASSWORD | WOLFSSH_USERAUTH_PUBLICKEY;
|
||||
}
|
||||
static byte output[1024], packet[1024];
|
||||
static word32 length;
|
||||
static WOLFSSH_CTX context = { authorize, result, reject_keyboard, allowed };
|
||||
static WOLFSSH ssh;
|
||||
static void reset(void) {
|
||||
memset(&ssh, 0, sizeof(ssh)); memset(output, 0, sizeof(output));
|
||||
memset(events, 0, sizeof(events)); event_count = 0;
|
||||
ssh.ctx = &context; ssh.outputBuffer.buffer = output; ssh.sessionIdSz = 32;
|
||||
auth_return = WOLFSSH_USERAUTH_SUCCESS; crypto_return = WS_SUCCESS;
|
||||
result_return = WS_SUCCESS; send_return = WS_SUCCESS; expect_new_password = 0;
|
||||
length = 0;
|
||||
}
|
||||
static void blob(const void *s, word32 n) {
|
||||
assert(length + 4 + n <= sizeof(packet));
|
||||
c32toa(n, packet + length); length += 4;
|
||||
memcpy(packet + length, s, n); length += n;
|
||||
}
|
||||
static void string(const char *s) { blob(s, (word32)strlen(s)); }
|
||||
static void request(const char *method) { string("test"); string("ssh-connection"); string(method); }
|
||||
static void key_request(int signed_key, const char *algorithm) {
|
||||
request("publickey"); packet[length++] = signed_key; string(algorithm);
|
||||
byte nested[128]; word32 n = (word32)strlen(algorithm);
|
||||
c32toa(n, nested); memcpy(nested + 4, algorithm, n); nested[4 + n] = 42;
|
||||
blob(nested, n + 5);
|
||||
if (signed_key) {
|
||||
c32toa(1, nested + 4 + n); nested[8 + n] = 42;
|
||||
blob(nested, n + 9);
|
||||
}
|
||||
}
|
||||
static int dispatch(void) { word32 idx = 0; return DoUserAuthRequest(&ssh, packet, length, &idx); }
|
||||
static void check(const char *trace, int done) {
|
||||
assert(!strcmp(events, trace));
|
||||
assert((ssh.clientState == CLIENT_USERAUTH_DONE) == done); ++groups;
|
||||
}
|
||||
int main(void) {
|
||||
const int rejected[] = { WOLFSSH_USERAUTH_INVALID_PUBLICKEY,
|
||||
WOLFSSH_USERAUTH_FAILURE, WOLFSSH_USERAUTH_REJECTED,
|
||||
WOLFSSH_USERAUTH_INVALID_USER, WOLFSSH_USERAUTH_INVALID_AUTHTYPE };
|
||||
const char *algorithms[] = { "ssh-ed25519", "ecdsa-sha2-nistp256" };
|
||||
for (unsigned a = 0; a < 2; ++a) {
|
||||
for (unsigned r = 0; r < sizeof(rejected)/sizeof(rejected[0]); ++r) {
|
||||
reset(); key_request(1, algorithms[a]); auth_return = rejected[r];
|
||||
assert(dispatch() == WS_SUCCESS); check("AF", 0);
|
||||
}
|
||||
reset(); key_request(0, algorithms[a]); assert(dispatch() == WS_SUCCESS); check("AP", 0);
|
||||
reset(); key_request(0, algorithms[a]); auth_return = WOLFSSH_USERAUTH_INVALID_PUBLICKEY;
|
||||
assert(dispatch() == WS_SUCCESS); check("AF", 0);
|
||||
reset(); key_request(1, algorithms[a]); crypto_return = WS_CRYPTO_FAILED;
|
||||
result_return = WS_ERROR; /* Failure-result return is ignored. */
|
||||
assert(dispatch() == WS_SUCCESS); check(a ? "AHCrF" : "ACrF", 0);
|
||||
reset(); key_request(1, algorithms[a]); assert(dispatch() == WS_SUCCESS);
|
||||
check(a ? "AHCR" : "ACR", 1);
|
||||
reset(); key_request(1, algorithms[a]); result_return = WS_ERROR;
|
||||
assert(dispatch() == WS_SUCCESS); check(a ? "AHCRF" : "ACRF", 0);
|
||||
reset(); key_request(1, algorithms[a]); auth_return = WOLFSSH_USERAUTH_WOULD_BLOCK;
|
||||
assert(dispatch() == WS_AUTH_PENDING); check("A", 0);
|
||||
}
|
||||
for (int fail = 0; fail < 2; ++fail) {
|
||||
reset(); request("password"); packet[length++] = 0; string("dummy");
|
||||
auth_return = fail ? WOLFSSH_USERAUTH_INVALID_PASSWORD : WOLFSSH_USERAUTH_SUCCESS;
|
||||
assert(dispatch() == WS_SUCCESS); check(fail ? "AF" : "A", !fail);
|
||||
}
|
||||
reset(); request("password"); packet[length++] = 1; string("dummy"); string("new-dummy");
|
||||
expect_new_password = 1; auth_return = WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
|
||||
assert(dispatch() == WS_SUCCESS); check("AF", 0);
|
||||
/* Actual parser still calls auth when the new-password length is truncated. */
|
||||
reset(); request("password"); packet[length++] = 1; string("dummy");
|
||||
expect_new_password = 1; auth_return = WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
|
||||
assert(dispatch() == WS_SUCCESS); check("AF", 0);
|
||||
const char *unsupported[] = { "none", "unrecognized" };
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
reset(); request(unsupported[i]); assert(dispatch() == WS_SUCCESS); check("F", 0);
|
||||
}
|
||||
reset(); key_request(1, "unsupported-key"); assert(dispatch() == WS_SUCCESS); check("F", 0);
|
||||
reset(); key_request(1, "ssh-ed25519"); --length;
|
||||
assert(dispatch() == WS_BUFFER_E); check("", 0);
|
||||
reset(); request("keyboard-interactive"); string(""); string("");
|
||||
assert(context.keyboardAuthCb != NULL); assert(dispatch() == WS_ERROR); check("KX", 0);
|
||||
/* The library writes the message byte even on rejection, but does not send. */
|
||||
assert(ssh.outputBuffer.length == 0 && output[0] == MSGID_USERAUTH_INFO_REQUEST);
|
||||
char methods[MAX_AUTH_STRING]; int n = GetAllowedAuth(&ssh, methods);
|
||||
methods[n] = '\0'; assert(!strcmp(methods, "publickey,password")); ++groups;
|
||||
|
||||
/* Execute actual copy/positive-consumed path, including deferred network send. */
|
||||
for (int deferred = 0; deferred < 2; ++deferred) {
|
||||
reset(); byte data[] = { 11, 22, 33, 44, 55 };
|
||||
channel = (WOLFSSH_CHANNEL){ 100, 3, 10, 7 };
|
||||
send_return = deferred ? WS_WANT_WRITE : WS_SUCCESS;
|
||||
assert(SendChannelData(&ssh, 1, data, sizeof(data)) == 3);
|
||||
assert(!memcmp(output + 9, data, 3)); memset(data, 0, 3);
|
||||
assert(output[9] == 11 && output[10] == 22 && output[11] == 33);
|
||||
assert(data[3] == 44 && channel.peerWindowSz == 97);
|
||||
assert(ssh.outputBuffer.plainSz == (deferred ? 3U : 0U)); check("TBS", 0);
|
||||
}
|
||||
reset(); ssh.outputBuffer.plainSz = 2; send_return = WS_WANT_WRITE;
|
||||
byte data[] = { 1, 2 }; assert(SendChannelData(&ssh, 1, data, 2) == WS_WANT_WRITE);
|
||||
assert(output[9] == 0); check("S", 0);
|
||||
printf("PASS: %u actual wolfSSH parser/control-flow cases\n", groups);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute extracted, hash-pinned wolfSSH control flow; no downloads or build writes."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
VENDOR = ROOT / "managed_components/wolfssl__wolfssh"
|
||||
REVIEWED_SHA256 = "81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9"
|
||||
|
||||
|
||||
def extract(source, name):
|
||||
# Mask comments/strings without changing offsets, then balance actual braces.
|
||||
masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'',
|
||||
lambda m: " " * len(m[0]), source, flags=re.S)
|
||||
matches = list(re.finditer(r"(?m)^(?:static )?(?:int|void|byte|word32)\s+" +
|
||||
re.escape(name) + r"\s*\([^;{}]*\)\s*\{", masked))
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError(f"Expected one definition of {name}, found {len(matches)}")
|
||||
start = matches[0].start()
|
||||
brace = masked.index("{", start)
|
||||
depth = 1
|
||||
end = brace + 1
|
||||
while depth:
|
||||
depth += (masked[end] == "{") - (masked[end] == "}")
|
||||
end += 1
|
||||
return source[start:end] + "\n"
|
||||
|
||||
|
||||
def check_build_profile(database):
|
||||
entries = json.loads(database.read_text())
|
||||
entry = next(e for e in entries if Path(e["file"]).resolve() ==
|
||||
(VENDOR / "src/internal.c").resolve())
|
||||
args = entry.get("arguments") or shlex.split(entry["command"])
|
||||
# Strip output/dependency-writing flags: this must only preprocess to stdout.
|
||||
clean = []
|
||||
skip = False
|
||||
for arg in args:
|
||||
if skip:
|
||||
skip = False
|
||||
elif arg in ("-o", "-MF", "-MT", "-MQ"):
|
||||
skip = True
|
||||
elif arg not in ("-c", "-MD", "-MMD", "-MP"):
|
||||
clean.append(arg)
|
||||
result = subprocess.run(clean + ["-E", "-dM"], cwd=entry["directory"],
|
||||
capture_output=True, text=True, check=True, timeout=30,
|
||||
env={**os.environ, "CCACHE_DISABLE": "1"})
|
||||
macros = dict(re.findall(r'^#define (\w+)(?: (.*))?$', result.stdout, re.M))
|
||||
if macros.get("LIBWOLFSSH_VERSION_HEX") != "0x01004020":
|
||||
raise RuntimeError("Resolved wolfSSH version differs from reviewed version")
|
||||
absent = ("WOLFSSH_CERTS", "WOLFSSH_ALLOW_USERAUTH_NONE", "WOLFSSH_NO_ECDSA",
|
||||
"WOLFSSH_NO_ED25519", "NO_FAILURE_ON_REJECTED")
|
||||
if "WOLFSSH_NO_RSA" not in macros or any(m in macros for m in absent):
|
||||
raise RuntimeError("Resolved wolfSSH auth feature profile changed; re-audit")
|
||||
print("PASS: actual compiler preprocessing matches reviewed auth feature profile", flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
databases = sorted((ROOT / ".pio/build").glob("*/compile_commands.json"))
|
||||
default_database = databases[0] if len(databases) == 1 else ROOT / "compile_commands.json"
|
||||
parser.add_argument("--compile-commands", type=Path, default=default_database)
|
||||
parser.add_argument("--host-only", action="store_true",
|
||||
help="explicitly skip production compile-command feature verification")
|
||||
options = parser.parse_args()
|
||||
raw = (VENDOR / "src/internal.c").read_bytes()
|
||||
actual = hashlib.sha256(raw).hexdigest()
|
||||
if actual != REVIEWED_SHA256:
|
||||
raise RuntimeError(f"wolfSSH internal.c changed: {actual}; re-audit before updating hash")
|
||||
version = (VENDOR / "wolfssh/version.h").read_text()
|
||||
if not re.search(r'#define\s+LIBWOLFSSH_VERSION_HEX\s+0x01004020\b', version):
|
||||
raise RuntimeError("Expected wolfSSH 1.4.20 header")
|
||||
if not re.search(r'#define\s+LIBWOLFSSH_VERSION_STRING\s+"1\.4\.20"', version):
|
||||
raise RuntimeError("Unexpected wolfSSH version string")
|
||||
manifest = (ROOT / "src/idf_component.yml").read_text()
|
||||
if not re.search(r'^\s*wolfssl/wolfssh:\s*"1\.4\.20"\s*$', manifest, re.M):
|
||||
raise RuntimeError("Application must pin wolfSSH exactly to 1.4.20")
|
||||
|
||||
if options.host_only:
|
||||
print("SKIP: production feature verification (--host-only)", flush=True)
|
||||
else:
|
||||
check_build_profile(options.compile_commands)
|
||||
|
||||
source = raw.decode()
|
||||
# Use the installed public callback data layouts, not hand-maintained copies.
|
||||
header = (VENDOR / "wolfssh/ssh.h").read_text()
|
||||
types = header[header.index("typedef struct WS_UserAuthData_Password {"):
|
||||
header.index("} WS_UserAuthData;") + len("} WS_UserAuthData;")]
|
||||
results_start = header.index("enum WS_UserAuthResults")
|
||||
types += "\n" + header[results_start:header.index("};", results_start) + 2]
|
||||
types += "\n" + "\n".join(re.findall(
|
||||
r'^#define WOLFSSH_USERAUTH_(?:PASSWORD|PUBLICKEY|KEYBOARD|NONE)\s+.*$', header, re.M))
|
||||
names = ["GetBoolean", "GetUint32", "GetSize", "GetStringRef",
|
||||
"DoUserAuthRequestPassword", "DoUserAuthRequestPublicKey",
|
||||
"SendUserAuthKeyboardRequest", "DoUserAuthRequest", "GetAllowedAuth",
|
||||
"SendChannelData"]
|
||||
extracted = "\n".join(extract(source, name) for name in names)
|
||||
with tempfile.TemporaryDirectory(prefix="wolfssh-auth-contract-") as temp:
|
||||
temp = Path(temp)
|
||||
(temp / "auth_types.h").write_text(types)
|
||||
(temp / "actual.c").write_text(extracted)
|
||||
binary = temp / "contract"
|
||||
cc = shlex.split(os.environ.get("CC", "cc"))
|
||||
subprocess.run(cc + ["-std=c99", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", "-I", str(temp),
|
||||
str(HERE / "contract.c"), "-o", str(binary)],
|
||||
check=True, timeout=30, env={**os.environ, "CCACHE_DISABLE": "1"})
|
||||
subprocess.run([str(binary)], check=True, timeout=10)
|
||||
print("PASS: installed source SHA-256, version header and exact application pin")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user