Harden SSH Admission And Credential Input
This commit is contained in:
@@ -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