Harden wolfSSL and wolfSSH validation
Enable validated ECC imports and X25519 all-zero rejection through PUBLIC build policy. Tighten wolfSSH parser bounds, overflow handling, and signature framing with guard-page and crypto vector contracts.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# Bounded wolfSSH parser contract
|
||||
|
||||
Run from the project root (installed pinned sources and a host C compiler required):
|
||||
|
||||
```sh
|
||||
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/run.py
|
||||
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py --host-only
|
||||
CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py
|
||||
```
|
||||
|
||||
No download, PlatformIO, managed-component edit, production build-tree regeneration,
|
||||
or device operation is performed. The runner verifies the original `internal.c`
|
||||
SHA-256, calls the production `render_entry`, writes and reads back its generated
|
||||
bytes in a temporary directory, and extracts complete actual functions. C tests
|
||||
run with guard pages and UBSan trap instrumentation, both with and without
|
||||
`WOLFSSH_SMALL_STACK`. The existing SDK suite separately tests generation and
|
||||
CMake source replacement using fixtures. This is **not** a claim that an existing
|
||||
production generated file or firmware binary contains these edits.
|
||||
|
||||
## Reviewed upstream evidence and exact implementation scope
|
||||
|
||||
Official diffs fetched and inspected on 2026-09-15:
|
||||
|
||||
- https://github.com/wolfSSL/wolfssh/pull/892.diff
|
||||
- https://github.com/wolfSSL/wolfssh/pull/881.diff
|
||||
- https://github.com/wolfSSL/wolfssh/pull/899.diff
|
||||
- https://github.com/wolfSSL/wolfssh/pull/880.diff
|
||||
|
||||
These are PR URLs, not immutable commit pins. The authoritative local inputs remain
|
||||
original wolfSSH **1.4.20**, SHA-256
|
||||
`81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`,
|
||||
plus the exact-once checked-in edits in `tools/security_overrides.py`. No repinning
|
||||
or wholesale upstream patch application occurs.
|
||||
|
||||
Covered:
|
||||
|
||||
- **892 subset:** `DoIgnore` calls `GetSkip`; `GetSkip` uses `GetSize` and accepts
|
||||
an empty string ending exactly at the payload boundary. `DoServiceRequest`
|
||||
validates the full string with `GetSize` before `GetString`, retaining the old
|
||||
strict `< WOLFSSH_MAX_NAMESZ` limit rather than upstream's truncation behavior.
|
||||
Failure leaves the caller index and client state untouched. Successful state
|
||||
transition remains exactly the old one; service-name semantic validation is
|
||||
not added. The original `GetSize` already uses bounded subtraction and needs
|
||||
no change. `GetString` now uses it and rejects zero output capacity before
|
||||
subtraction/copy; ordinary bounded truncation semantics remain unchanged.
|
||||
- **881 subset:** `DoChannelWindowAdjust` rejects addition exceeding the 32-bit
|
||||
maximum with `WS_OVERFLOW_E`, leaving the channel window unchanged. The parsed
|
||||
index still advances, as upstream does. No new include is needed for the
|
||||
explicit word32 maximum. Unknown channels and truncated fields stay rejected.
|
||||
- **880 subset:** both key/signature type checks in `DoUserAuthRequestEcc` and
|
||||
`DoUserAuthRequestEd25519` use OR.
|
||||
`GetSize` bounds the incoming span first; unequal lengths short-circuit before
|
||||
`memcmp`, and equal lengths compare exactly the expected span. Valid matching
|
||||
ECDSA/Ed25519 types follow the original crypto path. Existing error normalization
|
||||
remains (`WS_CRYPTO_FAILED` for key parsing, `WS_INVALID_ALGO_ID` for signature
|
||||
type mismatch).
|
||||
- **Local signature-framing correction:** the ECC r/s parser uses the checked
|
||||
end of the declared signature sub-blob, not the enclosing field size. The
|
||||
preceding `GetSize` establishes `sz <= signatureSz - i`, so calculating that
|
||||
end cannot wrap. Both mpints must exactly consume the sub-blob, and the
|
||||
sub-blob must exactly consume the enclosing signature field. Ed25519 likewise
|
||||
rejects bytes outside its declared signature string before starting message
|
||||
verification. These framing errors return `WS_BUFFER_E`. This intentionally
|
||||
rejects previously tolerated malformed trailing bytes; valid SSH signature
|
||||
framing and the crypto calls/digest/message construction are unchanged.
|
||||
This is a local correction verified against the pinned implementation and
|
||||
[key-validation review](../../docs/ssh_key_validation_review.md), not a claim
|
||||
that these framing edits came from PR 880.
|
||||
|
||||
Reachability evidence: the pinned `DoPacket` dispatches IGNORE, SERVICE_REQUEST,
|
||||
and CHANNEL_WINDOW_ADJUST to these handlers. `DoUserAuthRequestPublicKey` calls
|
||||
`DoUserAuthRequestEcc`/`DoUserAuthRequestEd25519` for ECDSA/Ed25519 authentication,
|
||||
both enabled in this server's reviewed profile. Advertisement is not treated as a parser dispatch filter.
|
||||
|
||||
## Explicitly deferred (not fixed by this slice)
|
||||
|
||||
- **892:** client `DoServiceAccept`, agent key preparation, daemon authentication,
|
||||
Windows terminal changes. Password framing/wiping is the existing local
|
||||
correction, intentionally not replaced with upstream's later formulation.
|
||||
- **899:** no hunks applied. `ParseRSAPubKey`/`ParseECCPubKey` skips require separate
|
||||
client/KEX reachability analysis (not the server's `DoUserAuthRequestEcc`).
|
||||
The old `DoChannelFailure` does not parse a channel ID at all; changing only its
|
||||
`len != 0` typo would not establish a bounded channel-ID parser. Its existing
|
||||
behavior is left unchanged rather than claiming the later parser contract.
|
||||
Windows port/terminal hunks are out of scope.
|
||||
- **880:** certificate RSA, agent, daemon, terminal, TPM and SCP changes are not
|
||||
applied. No complete PR-880 closure is claimed.
|
||||
- Message ordering/state machine (including CVE-2025-14942), service semantics,
|
||||
standalone ECC curve-name/key-blob semantic validation, other parsers, client
|
||||
behavior and broader crypto advisories are outside this slice. ECC point/import
|
||||
validation belongs to the separate crypto-policy owner and is not changed here.
|
||||
Existing account/key authorization, numeric r/s validity and Ed25519 raw
|
||||
signature-size/crypto validity checks remain owned by their existing layers.
|
||||
|
||||
## Test boundaries
|
||||
|
||||
The C matrix exercises zero/truncated/exact/oversized/wrapping lengths, invalid
|
||||
and nonzero offsets, zero-capacity output, copy canaries, window overflow boundary
|
||||
pairs, unknown channels, ECC equal-length mismatches, shorter/longer matching
|
||||
prefixes, empty types and every key/signature truncation. Expected ECC and
|
||||
Ed25519 type bytes end at a protected page, testing unequal-length short-circuit
|
||||
safety. Crypto and
|
||||
channel lookup are doubles; tests establish parser gating, not real signature or
|
||||
point validation. Numeric errors, context/channel layouts and name capacity are
|
||||
host doubles, not production ABI verification.
|
||||
|
||||
The follow-up matrix in `auth_framing.c` covers every truncated ECC sub-blob
|
||||
boundary with a complete r/s pair still available beyond that boundary, oversized
|
||||
and wrapping nested lengths, malformed r/s lengths, inner/outer trailing bytes,
|
||||
and physically guard-page-ended frames. Instrumented `ato32` also asserts that
|
||||
nested length reads cannot use accessible bytes outside the sub-blob. Signature
|
||||
input and surrounding canaries stay unchanged. Valid 32-byte and sign-padded
|
||||
33-byte r/s encodings reach conversion with their bytes/lengths intact. Ed25519
|
||||
covers both labels, all truncations, shortened/oversized/wrapping/trailing
|
||||
signature strings, exact raw-signature forwarding and unchanged streamed message
|
||||
bytes. Both paths retain crypto rejection behavior using doubles.
|
||||
|
||||
Validation: **3,124 cases per stack mode** (both pass with UBSan trap mode), plus
|
||||
**six guard-removal mutations rejected**: ECC nested read bound, inner/outer exact
|
||||
consumption, Ed25519 key/signature OR checks, and Ed25519 exact consumption. The
|
||||
mutation copies exist only in temporary test files; core dumps are disabled for
|
||||
those intentionally failing runs. These are framing-valid fixtures with crypto
|
||||
doubles, not independently verified real signatures.
|
||||
|
||||
The runner also compares complete password, packet dispatch, public-key dispatch and selected deferred
|
||||
functions against the pre-slice generated baseline to fence accidental changes.
|
||||
The separate auth suite executes its 135 password/control-flow cases, including
|
||||
payload wipe, callback framing and asynchronous pending retention. No whole-library
|
||||
fuzzing, real SSH exchange, firmware compile, hardware timing or security sign-off
|
||||
is implied.
|
||||
@@ -0,0 +1,213 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only
|
||||
* Included after actual generated parsers and the shared host doubles.
|
||||
* Signature bytes have valid SSH framing; crypto is intentionally doubled. */
|
||||
static void check_ecc_frame(WS_UserAuthData_PublicKey pk, const byte *frame,
|
||||
word32 n, byte *end, int good)
|
||||
{
|
||||
byte storage[512], before[512], digest[32]={0};
|
||||
assert(n+32<=sizeof(storage));
|
||||
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx};
|
||||
/* Accessible canaries catch writes; a second run ends at a guard page. */
|
||||
for (int guarded=0; guarded<2; guarded++) {
|
||||
memset(storage,0xa5,sizeof(storage));
|
||||
byte *p=guarded?end-n:storage+16;
|
||||
memcpy(p,frame,n);
|
||||
memcpy(before,storage,sizeof(storage));
|
||||
pk.signature=p; pk.signatureSz=n;
|
||||
/* Instrument actual length reads as well as crypto calls: accessible
|
||||
* bytes after a short sub-blob must not even supply the s header. */
|
||||
word32 inner=8+pk.publicKeyTypeSz, declared=0;
|
||||
if (n>=inner) {
|
||||
ato32(frame+inner-4,&declared);
|
||||
if (declared<=n-inner) {
|
||||
nested_begin=(uintptr_t)(p+inner);
|
||||
nested_end=nested_begin+declared;
|
||||
nested_outer_end=(uintptr_t)(p+n);
|
||||
}
|
||||
}
|
||||
imports=converts=verifies=0;
|
||||
int ret=DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest));
|
||||
nested_begin=nested_end=nested_outer_end=0;
|
||||
assert(ret==(good?WS_SUCCESS:WS_BUFFER_E));
|
||||
assert(imports==1 && converts==(unsigned)good && verifies==(unsigned)good);
|
||||
assert(memcmp(storage,before,sizeof(storage))==0);
|
||||
assert(memcmp(p,frame,n)==0);
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
|
||||
static void ecc_framing(byte *end)
|
||||
{
|
||||
const byte type[]="ecdsa-sha2-nistp256";
|
||||
byte key[128]={0}, frame[256], r[33], s[33];
|
||||
word32 k=string(key,type,sizeof(type)-1);
|
||||
k+=string(key+k,(const byte*)"nistp256",8);
|
||||
k+=string(key+k,(const byte*)"Q",1);
|
||||
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeySz=k,
|
||||
.publicKeyType=type,.publicKeyTypeSz=sizeof(type)-1};
|
||||
/* Both ordinary positive mpints and leading-zero sign padding. */
|
||||
for (word32 rn=32;rn<=33;rn++) for (word32 sn=32;sn<=33;sn++) {
|
||||
memset(r,0x61,sizeof(r)); memset(s,0x62,sizeof(s));
|
||||
if (rn==33) { r[0]=0; r[1]=0x80; }
|
||||
if (sn==33) { s[0]=0; s[1]=0x80; }
|
||||
memset(frame,0x5a,sizeof(frame));
|
||||
word32 length_at=string(frame,type,sizeof(type)-1);
|
||||
word32 inner=length_at+4, blob=8+rn+sn;
|
||||
put(frame+length_at,blob);
|
||||
word32 r_at=inner, s_at=inner+4+rn;
|
||||
string(frame+r_at,r,rn); string(frame+s_at,s,sn);
|
||||
word32 total=inner+blob;
|
||||
expected_r=r; expected_s=s; expected_r_sz=rn; expected_s_sz=sn;
|
||||
check_ecc_frame(pk,frame,total,end,1);
|
||||
|
||||
/* All cuts within the nested blob. Backing bytes still contain a
|
||||
* complete r/s pair: old code consumed beyond the declared boundary. */
|
||||
for (word32 declared=0;declared<=blob+2;declared++) {
|
||||
put(frame+length_at,declared);
|
||||
check_ecc_frame(pk,frame,total,end,declared==blob);
|
||||
}
|
||||
const word32 huge[]={UINT32_MAX,UINT32_MAX-inner+1,0x80000000};
|
||||
for (unsigned j=0;j<sizeof(huge)/sizeof(*huge);j++) {
|
||||
put(frame+length_at,huge[j]);
|
||||
check_ecc_frame(pk,frame,total,end,0);
|
||||
}
|
||||
put(frame+length_at,blob);
|
||||
/* Every physical/logical truncation after the valid label, including
|
||||
* partial nested length headers and both mpints. */
|
||||
for (word32 cut=length_at;cut<total;cut++)
|
||||
check_ecc_frame(pk,frame,cut,end,0);
|
||||
|
||||
/* A complete pair plus trailing bytes, inside or outside the declared
|
||||
* sub-blob, must not reach conversion/verification. */
|
||||
for (word32 extra=1;extra<=8;extra++) {
|
||||
put(frame+length_at,blob);
|
||||
check_ecc_frame(pk,frame,total+extra,end,0);
|
||||
put(frame+length_at,blob+extra);
|
||||
check_ecc_frame(pk,frame,total+extra,end,0);
|
||||
}
|
||||
put(frame+length_at,blob);
|
||||
for (unsigned which=0;which<2;which++) {
|
||||
word32 at=which?s_at:r_at, real=which?sn:rn;
|
||||
word32 wrong[]={0,1,real-1,real+1,blob,UINT32_MAX,0xfffffffc};
|
||||
for (unsigned j=0;j<sizeof(wrong)/sizeof(*wrong);j++) {
|
||||
put(frame+at,wrong[j]);
|
||||
check_ecc_frame(pk,frame,total,end,0);
|
||||
}
|
||||
put(frame+at,real);
|
||||
}
|
||||
/* A correctly framed signature still propagates crypto rejection. */
|
||||
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; byte digest[32]={0};
|
||||
pk.signature=frame; pk.signatureSz=total;
|
||||
verify_failure=1; converts=verifies=0;
|
||||
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,32)==WS_ECC_E);
|
||||
assert(converts==1 && verifies==1); verify_failure=0; cases++;
|
||||
}
|
||||
expected_r=expected_s=NULL;
|
||||
}
|
||||
|
||||
static void reset_ed(void)
|
||||
{ ed_imports=ed_starts=ed_updates=ed_finals=0; }
|
||||
|
||||
static void check_ed_frame(WS_UserAuthData_PublicKey pk, const byte *frame,
|
||||
word32 n, byte *end, int good)
|
||||
{
|
||||
byte storage[512], before[512], data[256], session[32];
|
||||
memset(data,0x7c,sizeof(data)); memset(session,0x23,sizeof(session));
|
||||
assert(n+32<=sizeof(storage));
|
||||
struct context ctx={0};
|
||||
WOLFSSH ssh={.ctx=&ctx,.sessionId=session,.sessionIdSz=sizeof(session)};
|
||||
WS_UserAuthData auth={.usernameSz=4,.serviceNameSz=14,.authNameSz=9};
|
||||
pk.dataToSign=data;
|
||||
word32 signedSz=4+14+9+1+pk.publicKeyTypeSz+pk.publicKeySz+20;
|
||||
assert(signedSz<=sizeof(data));
|
||||
for (int guarded=0;guarded<2;guarded++) {
|
||||
memset(storage,0xa5,sizeof(storage));
|
||||
byte *p=guarded?end-n:storage+16;
|
||||
memcpy(p,frame,n); memcpy(before,storage,sizeof(storage));
|
||||
pk.signature=p; pk.signatureSz=n;
|
||||
reset_ed();
|
||||
int ret=DoUserAuthRequestEd25519(&ssh,&pk,&auth);
|
||||
assert(ret==(good?WS_SUCCESS:WS_BUFFER_E));
|
||||
assert(ed_imports==1 && ed_starts==(unsigned)good);
|
||||
assert(ed_updates==(good?4U:0U) && ed_finals==(unsigned)good);
|
||||
if (good) {
|
||||
byte expected[512];
|
||||
put(expected,sizeof(session)); memcpy(expected+4,session,sizeof(session));
|
||||
expected[4+sizeof(session)]=MSGID_USERAUTH_REQUEST;
|
||||
memcpy(expected+5+sizeof(session),data,signedSz);
|
||||
assert(ed_message_sz==5+sizeof(session)+signedSz);
|
||||
assert(memcmp(ed_message,expected,ed_message_sz)==0);
|
||||
}
|
||||
assert(memcmp(storage,before,sizeof(storage))==0);
|
||||
assert(memcmp(p,frame,n)==0);
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
|
||||
static void ed25519_framing(byte *end)
|
||||
{
|
||||
const byte type[]="ssh-ed25519";
|
||||
const word32 typeSz=sizeof(type)-1;
|
||||
byte key[128], frame[256], rawkey[32], rawsig[64], data[256]={0};
|
||||
memset(rawkey,0x45,sizeof(rawkey)); memset(rawsig,0x76,sizeof(rawsig));
|
||||
expected_ed_sig=rawsig; expected_ed_sig_sz=sizeof(rawsig);
|
||||
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; WS_UserAuthData auth={0};
|
||||
/* Both label checks, expected type ends at protected memory. */
|
||||
byte *expected=end-typeSz; memcpy(expected,type,typeSz);
|
||||
for (unsigned which=0;which<2;which++) for(unsigned mode=0;mode<6;mode++) {
|
||||
byte bad[40]={0}; memcpy(bad,type,typeSz); word32 n=typeSz;
|
||||
if(mode==1) bad[0]='X';
|
||||
if(mode==2) n--;
|
||||
if(mode==3) n++;
|
||||
if(mode==4) n=0;
|
||||
if(mode==5) n=sizeof(bad);
|
||||
word32 k=string(key,which==0?bad:type,which==0?n:typeSz);
|
||||
k+=string(key+k,rawkey,sizeof(rawkey));
|
||||
word32 f=string(frame,which==1?bad:type,which==1?n:typeSz);
|
||||
f+=string(frame+f,rawsig,sizeof(rawsig));
|
||||
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeyType=expected,
|
||||
.signature=frame,.publicKeySz=k,.publicKeyTypeSz=typeSz,
|
||||
.signatureSz=f,.dataToSign=data};
|
||||
reset_ed();
|
||||
int ret=DoUserAuthRequestEd25519(&ssh,&pk,&auth);
|
||||
if(mode==0) {
|
||||
assert(ret==0 && ed_imports==1 && ed_starts==1 && ed_updates==4 && ed_finals==1);
|
||||
}
|
||||
else {
|
||||
assert(ret==(which==0?WS_CRYPTO_FAILED:WS_INVALID_ALGO_ID));
|
||||
assert(ed_imports==which && ed_starts==0 && ed_updates==0 && ed_finals==0);
|
||||
}
|
||||
cases++;
|
||||
}
|
||||
word32 k=string(key,type,typeSz); k+=string(key+k,rawkey,sizeof(rawkey));
|
||||
memset(frame,0x5a,sizeof(frame));
|
||||
word32 length_at=string(frame,type,typeSz);
|
||||
word32 total=length_at+string(frame+length_at,rawsig,sizeof(rawsig));
|
||||
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeyType=type,
|
||||
.signature=frame,.publicKeySz=k,.publicKeyTypeSz=typeSz,
|
||||
.signatureSz=total,.dataToSign=data};
|
||||
for (word32 declared=0;declared<=66;declared++) {
|
||||
put(frame+length_at,declared);
|
||||
check_ed_frame(pk,frame,total,end,declared==64);
|
||||
}
|
||||
const word32 huge[]={UINT32_MAX,UINT32_MAX-length_at,0x80000000};
|
||||
for (unsigned j=0;j<sizeof(huge)/sizeof(*huge);j++) {
|
||||
put(frame+length_at,huge[j]); check_ed_frame(pk,frame,total,end,0);
|
||||
}
|
||||
put(frame+length_at,64);
|
||||
for (word32 cut=0;cut<total;cut++)
|
||||
check_ed_frame(pk,frame,cut,end,0);
|
||||
for (word32 extra=1;extra<=8;extra++)
|
||||
check_ed_frame(pk,frame,total+extra,end,0);
|
||||
/* Key truncations must not import or start signature verification. */
|
||||
for (word32 cut=0;cut<k;cut++) {
|
||||
byte *p=end-cut; memcpy(p,key,cut);
|
||||
pk.publicKey=p; pk.publicKeySz=cut; reset_ed();
|
||||
assert(DoUserAuthRequestEd25519(&ssh,&pk,&auth)==WS_CRYPTO_FAILED);
|
||||
assert(ed_imports==0 && ed_starts==0 && ed_finals==0); cases++;
|
||||
}
|
||||
pk.publicKey=key; pk.publicKeySz=k; reset_ed(); verify_failure=1;
|
||||
assert(DoUserAuthRequestEd25519(&ssh,&pk,&auth)==WS_ED25519_E);
|
||||
assert(ed_starts==1 && ed_updates==4 && ed_finals==1);
|
||||
verify_failure=0; expected_ed_sig=NULL; cases++;
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only
|
||||
* Extracted wolfSSH functions retain upstream GPL notices in generated source.
|
||||
* Crypto doubles test parser gating, not cryptographic validity. */
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
typedef uint8_t byte;
|
||||
typedef uint32_t word32;
|
||||
#define WS_SUCCESS 0
|
||||
#define WS_BUFFER_E -1
|
||||
#define WS_BAD_ARGUMENT -2
|
||||
#define WS_INVALID_CHANID -3
|
||||
#define WS_OVERFLOW_E -4
|
||||
#define WS_MEMORY_E -5
|
||||
#define WS_INVALID_ALGO_ID -6
|
||||
#define WS_CRYPTO_FAILED -7
|
||||
#define WS_ECC_E -8
|
||||
#define WS_ED25519_E -9
|
||||
#define MSGID_USERAUTH_REQUEST 50
|
||||
#define MSG_ID_SZ 1
|
||||
#define BOOLEAN_SZ 1
|
||||
#define Ed25519 0
|
||||
#define UINT32_SZ 4
|
||||
#define WOLFSSH_MAX_NAMESZ 32
|
||||
#define CLIENT_USERAUTH_REQUEST_DONE 42
|
||||
#define WS_CHANNEL_ID_SELF 0
|
||||
#define WLOG(...) ((void)0)
|
||||
#define WOLFSSH_UNUSED(x) ((void)(x))
|
||||
#define WMEMCPY memcpy
|
||||
#define WMEMCMP memcmp
|
||||
#define WMALLOC(n,h,t) malloc(n)
|
||||
#define WFREE(p,h,t) free(p)
|
||||
#define ECDSA_ASN_SIG_SZ 80
|
||||
#define INVALID_DEVID -1
|
||||
#define WC_SIGNATURE_TYPE_ECC 1
|
||||
struct context { void *heap; };
|
||||
typedef struct {
|
||||
struct context *ctx;
|
||||
int clientState;
|
||||
const byte *sessionId;
|
||||
word32 sessionIdSz;
|
||||
} WOLFSSH;
|
||||
typedef struct { word32 peerWindowSz; } WOLFSSH_CHANNEL;
|
||||
typedef struct {
|
||||
const byte *publicKey, *publicKeyType, *signature;
|
||||
word32 publicKeySz, publicKeyTypeSz, signatureSz;
|
||||
const byte *dataToSign;
|
||||
} WS_UserAuthData_PublicKey;
|
||||
typedef struct {
|
||||
word32 usernameSz, serviceNameSz, authNameSz;
|
||||
} WS_UserAuthData;
|
||||
typedef struct { int unused; } ed25519_key;
|
||||
typedef struct { int unused; } ecc_key;
|
||||
enum wc_HashType { HASH_SHA256 };
|
||||
static unsigned imports, converts, verifies, finds, cases;
|
||||
static unsigned ed_imports, ed_starts, ed_updates, ed_finals;
|
||||
static const byte *expected_r, *expected_s, *expected_ed_sig;
|
||||
static word32 expected_r_sz, expected_s_sz, expected_ed_sig_sz;
|
||||
static byte ed_message[512];
|
||||
static word32 ed_message_sz;
|
||||
static int verify_failure;
|
||||
static uintptr_t nested_begin, nested_end, nested_outer_end;
|
||||
static WOLFSSH_CHANNEL channel;
|
||||
static WOLFSSH_CHANNEL *ChannelFind(WOLFSSH *ssh, word32 id, int side)
|
||||
{ finds++; return id == 7 ? &channel : NULL; }
|
||||
static void ato32(const byte *p, word32 *v)
|
||||
{
|
||||
uintptr_t address=(uintptr_t)p;
|
||||
if (nested_begin != 0 && address>=nested_begin && address<nested_outer_end)
|
||||
assert(address<=nested_end && 4<=nested_end-address);
|
||||
*v = ((word32)p[0]<<24) | ((word32)p[1]<<16) | ((word32)p[2]<<8) | p[3];
|
||||
}
|
||||
static void put(byte *p, word32 v)
|
||||
{ p[0]=v>>24; p[1]=v>>16; p[2]=v>>8; p[3]=v; }
|
||||
static int wc_ecc_init_ex(ecc_key *k, void *h, int id) { return 0; }
|
||||
static int wc_ecc_import_x963(const byte *p, word32 n, ecc_key *k)
|
||||
{ imports++; return 0; }
|
||||
static void wc_ecc_free(ecc_key *k) {}
|
||||
static int wc_ecc_rs_raw_to_sig(const byte *r, word32 rn, const byte *s,
|
||||
word32 sn, byte *out, word32 *n)
|
||||
{
|
||||
converts++;
|
||||
if (expected_r != NULL) {
|
||||
assert(rn==expected_r_sz && sn==expected_s_sz);
|
||||
assert(memcmp(r,expected_r,rn)==0 && memcmp(s,expected_s,sn)==0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int wc_SignatureVerifyHash(enum wc_HashType h, int t, byte *d,
|
||||
word32 dn, byte *s, word32 sn, ecc_key *k, size_t kn)
|
||||
{ verifies++; return verify_failure; }
|
||||
static void c32toa(word32 v, byte *p) { put(p,v); }
|
||||
static int wc_ed25519_init_ex(ed25519_key *key, void *heap, int id) { return 0; }
|
||||
static void wc_ed25519_free(ed25519_key *key) {}
|
||||
static int wc_ed25519_import_public(const byte *p, word32 n, ed25519_key *key)
|
||||
{ ed_imports++; assert(n==32); return 0; }
|
||||
static int wc_ed25519_verify_msg_init(const byte *sig, word32 n,
|
||||
ed25519_key *key, byte type, const byte *context, byte contextSz)
|
||||
{
|
||||
ed_starts++;
|
||||
assert(n==expected_ed_sig_sz && memcmp(sig,expected_ed_sig,n)==0);
|
||||
ed_message_sz=0;
|
||||
return 0;
|
||||
}
|
||||
static int wc_ed25519_verify_msg_update(const byte *p, word32 n, ed25519_key *key)
|
||||
{
|
||||
ed_updates++;
|
||||
assert(n<=sizeof(ed_message)-ed_message_sz);
|
||||
if (n != 0)
|
||||
memcpy(ed_message+ed_message_sz,p,n);
|
||||
ed_message_sz+=n;
|
||||
return 0;
|
||||
}
|
||||
static int wc_ed25519_verify_msg_final(const byte *sig, word32 n,
|
||||
int *status, ed25519_key *key)
|
||||
{
|
||||
ed_finals++;
|
||||
assert(n==expected_ed_sig_sz && memcmp(sig,expected_ed_sig,n)==0);
|
||||
*status=!verify_failure;
|
||||
return 0;
|
||||
}
|
||||
#include "actual.c"
|
||||
|
||||
static void parsers(byte *end)
|
||||
{
|
||||
struct context ctx = {0}; WOLFSSH ssh = {.ctx=&ctx, .clientState=9};
|
||||
const word32 lengths[] = {0,1,2,3,4,27,28,31,32,33,255,UINT32_MAX-4,UINT32_MAX};
|
||||
for (word32 n=0; n<=64; n++) {
|
||||
byte *p=end-n;
|
||||
for (unsigned j=0; j<sizeof(lengths)/sizeof(*lengths); j++) {
|
||||
memset(p, 'x', n);
|
||||
if (n>=4) put(p,lengths[j]);
|
||||
word32 idx=0, value=123;
|
||||
int good=n>=4 && lengths[j]<=n-4;
|
||||
assert(GetSize(&value,p,n,&idx)==(good?0:WS_BUFFER_E));
|
||||
idx=0;
|
||||
assert(DoIgnore(&ssh,p,n,&idx)==(good?0:WS_BUFFER_E));
|
||||
if (good) assert(idx==4+lengths[j]);
|
||||
idx=0; ssh.clientState=9;
|
||||
int service=good && lengths[j]<WOLFSSH_MAX_NAMESZ;
|
||||
assert(DoServiceRequest(&ssh,p,n,&idx)==(service?0:WS_BUFFER_E));
|
||||
assert(ssh.clientState==(service?42:9));
|
||||
assert(idx==(service?4+lengths[j]:0));
|
||||
char out[10]; memset(out, 0x55, sizeof(out));
|
||||
word32 cap=8; idx=0;
|
||||
assert(GetString(out+1,&cap,p,n,&idx)==(good?0:WS_BUFFER_E));
|
||||
assert(out[0]==0x55 && out[9]==0x55);
|
||||
if(good) { assert(cap==(lengths[j]<8?lengths[j]:7)); assert(out[cap+1]==0); }
|
||||
cap=0; idx=0;
|
||||
assert(GetString((char*)end,&cap,p,n,&idx)==WS_BUFFER_E);
|
||||
assert(idx==0);
|
||||
cases++;
|
||||
}
|
||||
word32 invalids[]={n,n+1,UINT32_MAX-3,UINT32_MAX};
|
||||
for(unsigned j=0;j<4;j++) {
|
||||
word32 idx=invalids[j], v=0;
|
||||
assert(GetSize(&v,p,n,&idx)==WS_BUFFER_E);
|
||||
idx=invalids[j]; assert(DoIgnore(&ssh,p,n,&idx)==WS_BUFFER_E);
|
||||
idx=invalids[j]; ssh.clientState=9;
|
||||
assert(DoServiceRequest(&ssh,p,n,&idx)==WS_BUFFER_E);
|
||||
assert(ssh.clientState==9 && idx==invalids[j]); cases++;
|
||||
}
|
||||
}
|
||||
/* Nonzero packet offsets and exact-end empty strings. */
|
||||
byte p[12]={0}; put(p+3,5); word32 idx=3;
|
||||
assert(DoIgnore(&ssh,p,12,&idx)==0 && idx==12);
|
||||
put(p+3,0); idx=3;
|
||||
assert(DoServiceRequest(&ssh,p,7,&idx)==0 && idx==7);
|
||||
}
|
||||
static void windows(byte *end)
|
||||
{
|
||||
WOLFSSH ssh={0};
|
||||
for(word32 n=0;n<8;n++) {
|
||||
byte *p=end-n; memset(p,0,n); word32 idx=0;
|
||||
channel.peerWindowSz=123; finds=0;
|
||||
assert(DoChannelWindowAdjust(&ssh,p,n,&idx)==WS_BUFFER_E);
|
||||
assert(channel.peerWindowSz==123 && finds==0 && idx==0); cases++;
|
||||
}
|
||||
word32 values[]={0,1,2,0x7fffffff,0xfffffffe,UINT32_MAX};
|
||||
for(unsigned a=0;a<6;a++) for(unsigned b=0;b<6;b++) {
|
||||
byte p[11]={0}; put(p+3,7); put(p+7,values[b]); word32 idx=3;
|
||||
channel.peerWindowSz=values[a];
|
||||
int overflow=values[b]>UINT32_MAX-values[a];
|
||||
assert(DoChannelWindowAdjust(&ssh,p,11,&idx)==(overflow?WS_OVERFLOW_E:0));
|
||||
assert(channel.peerWindowSz==(overflow?values[a]:values[a]+values[b]));
|
||||
assert(idx==11); cases++;
|
||||
}
|
||||
byte p[8]={0}; word32 idx=0; channel.peerWindowSz=12;
|
||||
assert(DoChannelWindowAdjust(&ssh,p,8,&idx)==WS_INVALID_CHANID);
|
||||
assert(channel.peerWindowSz==12);
|
||||
}
|
||||
static word32 string(byte *p, const byte *s, word32 n)
|
||||
{ put(p,n); memcpy(p+4,s,n); return n+4; }
|
||||
static void ecc(byte *end)
|
||||
{
|
||||
const byte type[]="ecdsa-sha2-nistp256";
|
||||
const word32 size=sizeof(type)-1;
|
||||
byte *expected=end-size; memcpy(expected,type,size);
|
||||
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; byte digest[32]={0};
|
||||
for(int which=0;which<2;which++) for(int mode=0;mode<6;mode++) {
|
||||
byte key[128]={0},sig[128]={0}, bad[40]={0};
|
||||
memcpy(bad,type,size); word32 n=size;
|
||||
if(mode==1) bad[0]='X'; /* equal length mismatch */
|
||||
if(mode==2) n--; /* matching prefix, shorter */
|
||||
if(mode==3) n++; /* expected ends at guard page */
|
||||
if(mode==4) n=0;
|
||||
if(mode==5) n=sizeof(bad);
|
||||
word32 k=string(key,which==0?bad:type,which==0?n:size);
|
||||
k+=string(key+k,(const byte*)"nistp256",8);
|
||||
k+=string(key+k,(const byte*)"Q",1);
|
||||
word32 s=string(sig,which==1?bad:type,which==1?n:size);
|
||||
put(sig+s,10); s+=4;
|
||||
s+=string(sig+s,(const byte*)"r",1);
|
||||
s+=string(sig+s,(const byte*)"s",1);
|
||||
WS_UserAuthData_PublicKey pk={.publicKey=key, .publicKeyType=expected,
|
||||
.signature=sig, .publicKeySz=k, .publicKeyTypeSz=size, .signatureSz=s};
|
||||
imports=converts=verifies=0;
|
||||
int ret=DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest));
|
||||
if(mode==0) assert(ret==0 && imports==1 && converts==1 && verifies==1);
|
||||
else {
|
||||
assert(ret==(which==0?WS_CRYPTO_FAILED:WS_INVALID_ALGO_ID));
|
||||
assert(imports==(unsigned)which && converts==0 && verifies==0);
|
||||
}
|
||||
cases++;
|
||||
if (mode==0 && which==0) {
|
||||
for (word32 cut=0; cut<k; cut++) {
|
||||
pk.publicKeySz=cut; verifies=0;
|
||||
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest))!=0);
|
||||
assert(verifies==0); cases++;
|
||||
}
|
||||
pk.publicKeySz=k;
|
||||
for (word32 cut=0; cut<s; cut++) {
|
||||
pk.signatureSz=cut; verifies=0;
|
||||
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest))!=0);
|
||||
assert(verifies==0); cases++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#include "auth_framing.c"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
long page=sysconf(_SC_PAGESIZE); assert(page>0);
|
||||
byte *map=mmap(NULL,(size_t)page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
|
||||
assert(map!=MAP_FAILED && mprotect(map+page,page,PROT_NONE)==0);
|
||||
parsers(map+page); windows(map+page); ecc(map+page);
|
||||
ecc_framing(map+page); ed25519_framing(map+page);
|
||||
assert(munmap(map,(size_t)page*2)==0);
|
||||
printf("PASS: %u parser/window/ECC/Ed25519 cases, guard pages + UBSan trap\n",cases);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Host contracts from freshly generated, original-hash-verified wolfSSH source."""
|
||||
import os
|
||||
import resource
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(ROOT / 'tests/wolfssh_auth_contract'))
|
||||
from run import extract
|
||||
from security_overrides import ENTRIES, render_entry
|
||||
|
||||
entry = next(e for e in ENTRIES if e.name == 'wolfssh_internal')
|
||||
assert entry.sha256 == '81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9'
|
||||
original, generated = render_entry(entry, {'project': ROOT})
|
||||
names = ('GetUint32', 'GetSize', 'GetString', 'GetSkip', 'GetStringRef',
|
||||
'DoIgnore', 'DoServiceRequest', 'DoChannelWindowAdjust', 'DoUserAuthRequestEcc',
|
||||
'DoUserAuthRequestEd25519')
|
||||
# Verify this slice cannot accidentally change ordering or existing password logic.
|
||||
from security_overrides import apply_edits, MODIFICATION_NOTICE, WOLFSSH_PARSER_EDITS
|
||||
baseline = MODIFICATION_NOTICE + apply_edits(original.read_text(), entry.edits[len(WOLFSSH_PARSER_EDITS):])
|
||||
for name in ('DoUserAuthRequestPassword', 'DoPacket', 'DoChannelFailure',
|
||||
'ParseRSAPubKey', 'ParseECCPubKey', 'DoUserAuthRequestPublicKey'):
|
||||
assert extract(generated.decode(), name) == extract(baseline, name), name
|
||||
with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory:
|
||||
work = Path(directory)
|
||||
# Read back the actual generated bytes, not a parallel implementation.
|
||||
source = work / 'internal.c'
|
||||
source.write_bytes(generated)
|
||||
functions = '\n'.join(extract(source.read_text(), n) for n in names)
|
||||
(work / 'actual.c').write_text(functions)
|
||||
for small in (False, True):
|
||||
binary = work / ('contract-small' if small else 'contract')
|
||||
subprocess.run(['cc', '-std=gnu11', '-O2', '-Wall', '-Wextra', '-Werror',
|
||||
'-Wno-unused-parameter', '-fsanitize=undefined',
|
||||
'-fsanitize-undefined-trap-on-error',
|
||||
*(['-DWOLFSSH_SMALL_STACK'] if small else []),
|
||||
'-I', str(work), str(HERE / 'contract.c'), '-o', str(binary)],
|
||||
check=True, timeout=30, env={**os.environ, 'CCACHE_DISABLE': '1'})
|
||||
subprocess.run([str(binary)], check=True, timeout=30)
|
||||
# Prove negative fixtures detect removal of each new boundary/type guard.
|
||||
# Mutations affect only temporary extracted host copies, never the override.
|
||||
mutations = (
|
||||
('ECC nested read boundary', 'DoUserAuthRequestEcc',
|
||||
(('pk->signature, sz, &i)', 'pk->signature, pk->signatureSz, &i)'),)),
|
||||
('ECC inner exact consumption', 'DoUserAuthRequestEcc',
|
||||
(('(i != sz || sz != pk->signatureSz)', '(sz != pk->signatureSz)'),)),
|
||||
('ECC outer exact consumption', 'DoUserAuthRequestEcc',
|
||||
(('(i != sz || sz != pk->signatureSz)', '(i != sz)'),)),
|
||||
('Ed25519 key label', 'DoUserAuthRequestEd25519',
|
||||
(('|| WMEMCMP(publicKeyType,', '&& WMEMCMP(publicKeyType,'),)),
|
||||
('Ed25519 signature label', 'DoUserAuthRequestEd25519',
|
||||
(('pk->publicKeyTypeSz ||', 'pk->publicKeyTypeSz &&'),)),
|
||||
('Ed25519 outer exact consumption', 'DoUserAuthRequestEd25519',
|
||||
(('if (ret == WS_SUCCESS && sz != pk->signatureSz - i)', 'if (0)'),)),
|
||||
)
|
||||
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
||||
for label, name, replacements in mutations:
|
||||
body = extract(source.read_text(), name)
|
||||
changed = body
|
||||
for old, new in replacements:
|
||||
assert old in changed, label
|
||||
changed = changed.replace(old, new)
|
||||
assert changed != body, label
|
||||
(work / 'actual.c').write_text(functions.replace(body, changed))
|
||||
binary = work / 'mutation'
|
||||
subprocess.run(['cc', '-std=gnu11', '-O2', '-Wall', '-Wextra', '-Werror',
|
||||
'-Wno-unused-parameter', '-I', str(work),
|
||||
str(HERE / 'contract.c'), '-o', str(binary)],
|
||||
check=True, timeout=30, env={**os.environ, 'CCACHE_DISABLE': '1'})
|
||||
result = subprocess.run([str(binary)], capture_output=True, timeout=30)
|
||||
assert result.returncode != 0, f'Undetected mutation: {label}'
|
||||
print(f'PASS: {len(mutations)} parser guard-removal mutations rejected')
|
||||
print('PASS: exact original hash; generated parser; unchanged ordering/password/deferred functions')
|
||||
print('NOTE: production build-tree registration/firmware not regenerated or validated')
|
||||
Reference in New Issue
Block a user