diff --git a/.github/workflows/sshd-test.yml b/.github/workflows/sshd-test.yml index c3e06d8e5..0469ce559 100644 --- a/.github/workflows/sshd-test.yml +++ b/.github/workflows/sshd-test.yml @@ -71,7 +71,25 @@ jobs: matrix: os: [ ubuntu-latest ] wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }} - mldsa: [ 'yes', 'no' ] + # The last two variants reuse the ML-DSA-enabled wolfSSL and build + # wolfSSH with extra defines it has no configure option for: one + # turns the composite algorithms off, one takes the small-stack + # (heap-allocated) composite paths. Those legs stop after the sshd + # tests; the later steps reconfigure without the extra define. + mldsa: [ 'yes', 'no', 'yes-no-composites', 'yes-small-stack' ] + include: + - mldsa: 'yes' + wolfssl_mldsa: 'yes' + extra_flags: '' + - mldsa: 'no' + wolfssl_mldsa: 'no' + extra_flags: '' + - mldsa: 'yes-no-composites' + wolfssl_mldsa: 'yes' + extra_flags: ' -DWOLFSSH_NO_MLDSA_COMPOSITES' + - mldsa: 'yes-small-stack' + wolfssl_mldsa: 'yes' + extra_flags: ' -DWOLFSSH_SMALL_STACK' name: Build and test the wolfsshd and wolfssh apps runs-on: ${{ matrix.os }} timeout-minutes: 10 @@ -80,7 +98,7 @@ jobs: uses: actions/cache@v5 with: path: build-dir/ - key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}-mldsa-${{ matrix.mldsa }}-v3 + key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}-mldsa-${{ matrix.wolfssl_mldsa }}-v3 fail-on-cache-miss: true - uses: actions/checkout@v6 @@ -94,7 +112,7 @@ jobs: - name: configure working-directory: ./wolfssh/ run : | - ./configure --enable-all --enable-ossh-certs LDFLAGS="-L${{ github.workspace }}/build-dir/lib" CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_NO_FPKI -DWOLFSSH_NO_SFTP_TIMEOUT -DWOLFSSH_MAX_SFTP_RW=4000000 -DMAX_PATH_SZ=120" + ./configure --enable-all --enable-ossh-certs LDFLAGS="-L${{ github.workspace }}/build-dir/lib" CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_NO_FPKI -DWOLFSSH_NO_SFTP_TIMEOUT -DWOLFSSH_MAX_SFTP_RW=4000000 -DMAX_PATH_SZ=120${{ matrix.extra_flags }}" - name: make check working-directory: ./wolfssh/ @@ -109,6 +127,7 @@ jobs: # could use optimization with caching - name: Test memory after close down working-directory: ./wolfssh/ + if: matrix.extra_flags == '' run: | sudo apt-get -y update sudo apt-get -y install valgrind @@ -123,6 +142,7 @@ jobs: # regression test, check that cat command does not hang - name: Test cat command for hanging working-directory: ./wolfssh/ + if: matrix.extra_flags == '' timeout-minutes: 1 run: | touch sshd_config.txt @@ -145,16 +165,19 @@ jobs: - name: configure with debug working-directory: ./wolfssh/ + if: matrix.extra_flags == '' run : | ./configure --enable-all --enable-debug LDFLAGS="-L${{ github.workspace }}/build-dir/lib" CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_NO_FPKI -DWOLFSSH_NO_SFTP_TIMEOUT -DWOLFSSH_MAX_SFTP_RW=4000000 -DMAX_PATH_SZ=120" - name: make working-directory: ./wolfssh/ + if: matrix.extra_flags == '' run: make # ssh_kex_algos.sh requires debug output otherwise it is skipped - name: Run wolfSSHd tests with debug working-directory: ./wolfssh/apps/wolfsshd/test + if: matrix.extra_flags == '' run: | git log -3 sudo ./run_all_sshd_tests.sh --match ssh_kex_algos.sh diff --git a/apps/wolfssh-options.c b/apps/wolfssh-options.c index 41a0e5a03..ba1d42531 100644 --- a/apps/wolfssh-options.c +++ b/apps/wolfssh-options.c @@ -147,9 +147,10 @@ int main(void) #ifndef WOLFSSH_NO_MLDSA printf("MLDSA\n"); #endif - /* Same guard as cannedKeyAlgoNamesHostKey in src/internal.c: the - * composite needs the ECDSA half too. */ -#if !defined(WOLFSSH_NO_MLDSA87) && \ + /* Same guard as cannedKeyAlgoNamesHostKey in src/internal.c: composites + * as a whole can be compiled out, and this one needs the ECDSA half. */ +#if !defined(WOLFSSH_NO_MLDSA_COMPOSITES) && \ + !defined(WOLFSSH_NO_MLDSA87) && \ !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) printf("MLDSA87_ES384\n"); #endif diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 0d64a0344..bd7d51e49 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -253,13 +253,33 @@ USER_NODE* AddNewUser(USER_NODE* list, byte type, const byte* username, } #endif +/* Big-endian uint32 read. ato32() is WOLFSSH_LOCAL, so it is unresolvable + * in a NO_INLINE build linked against a shared libwolfssh. */ +static word32 AuthReadU32(const byte* c) +{ + return ((word32)c[0] << 24) | ((word32)c[1] << 16) | + ((word32)c[2] << 8) | (word32)c[3]; +} + +/* Maps signature algorithms to key types (e.g. RSA SHA-2 to ssh-rsa). */ +static const char* AuthKeysTokenKeyType(const char* type) +{ + if (WSTRCMP(type, "rsa-sha2-256") == 0 || + WSTRCMP(type, "rsa-sha2-512") == 0) { + return "ssh-rsa"; + } + + return type; +} + /* TODO: Can use wolfSSH_ReadKey_buffer? */ +/* isCert skips the wire-format type/embedded-type cross-check. */ #ifdef WOLFSSHD_UNIT_TEST int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, - word32 keySz) + word32 keySz, int isCert) #else static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, - word32 keySz) + word32 keySz, int isCert) #endif { int ret = WSSHD_AUTH_SUCCESS; @@ -270,41 +290,22 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, word32 keyCandSz = 0; char* last = NULL; - /* Valid key types come from the same TYPE_KEY name registry - * (NameIdMap) that KEX negotiation uses, via wolfSSH_QueryKey(), - * instead of a separately hand-maintained list that could drift - * out of sync with it. */ - int typeOk = 0; - word32 queryIdx = 0; - const char* algoName; - if (line == NULL || lineSz == 0 || key == NULL || keySz == 0) { ret = WS_BAD_ARGUMENT; } if (ret == WSSHD_AUTH_SUCCESS) { + /* Skip truncated or whitespace-only lines. */ if ((type = WSTRTOK(line, " ", &last)) == NULL) { - ret = WS_FATAL_ERROR; + ret = WSSHD_AUTH_FAILURE; } else if ((keyCandBase64 = WSTRTOK(NULL, " ", &last)) == NULL) { - ret = WS_FATAL_ERROR; + ret = WSSHD_AUTH_FAILURE; } } if (ret == WSSHD_AUTH_SUCCESS) { - while ((algoName = wolfSSH_QueryKey(&queryIdx)) != NULL) { - /* OpenSSH cert types are verified via the CA path, not by - * literal comparison here; exclude them. */ - if (WSTRSTR(algoName, "-cert-v01@openssh.com") != NULL) { - continue; - } - if (WSTRCMP(type, algoName) == 0) { - typeOk = 1; - break; - } - } - if (!typeOk) { - /* Skip unsupported key types so the scan continues to later - * entries instead of aborting the whole file. */ + /* Cert types are verified via CA path, skip literal comparison. */ + if (WSTRSTR(type, "-cert-v01@openssh.com") != NULL) { ret = WSSHD_AUTH_FAILURE; } } @@ -318,10 +319,34 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, else { if (Base64_Decode((byte*)keyCandBase64, keyCandBase64Sz, keyCand, &keyCandSz) != 0) { - ret = WS_FATAL_ERROR; + /* Skip non-base64 tokens (e.g. option-prefixed lines). */ + ret = WSSHD_AUTH_FAILURE; } } } + if (ret == WSSHD_AUTH_SUCCESS && !isCert) { + /* Skip cross-check for raw DER certificate blobs. */ + word32 typeStrSz; + const char* keyType = AuthKeysTokenKeyType(type); + word32 keyTypeSz = (word32)XSTRLEN(keyType); + + if (keyCandSz >= 4) { + typeStrSz = AuthReadU32(keyCand); + if (typeStrSz != keyTypeSz || typeStrSz > keyCandSz - 4 || + XMEMCMP(keyType, keyCand + 4, keyTypeSz) != 0) { + /* Skip: token type doesn't match embedded key blob type. */ + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Skipping key line, type %s " + "does not match the type embedded in this line's key " + "blob", type); + ret = WSSHD_AUTH_FAILURE; + } + } + else { + wolfSSH_Log(WS_LOG_DEBUG, + "[SSHD] Skipping key line, blob too short for a type field"); + ret = WSSHD_AUTH_FAILURE; + } + } if (ret == WSSHD_AUTH_SUCCESS) { /* Constant-time compare to avoid leaking which prefix bytes of an * authorized key match a candidate offered by a remote peer. */ @@ -1331,11 +1356,11 @@ int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid, #endif } -/* Scan a resolved keys file (authorized_keys or TrustedUserCAKeys) for - * (key, keySz). Fails closed with WSSHD_AUTH_FAILURE when no line matches. - * strictModes opens through the secure gate; the file must be owned by uid. */ +/* Scan keys file. Fails closed on no match. + * strictModes requires uid ownership. isCert flags DER vs wire-format. */ static int SearchKeysFile(const char* keysFilePath, const byte* key, - word32 keySz, WUID_T uid, int strictModes) + word32 keySz, WUID_T uid, int strictModes, + int isCert) { int ret = WSSHD_AUTH_SUCCESS; WFILE *f = WBADFILE; @@ -1389,7 +1414,7 @@ static int SearchKeysFile(const char* keysFilePath, const byte* key, continue; /* commented out line */ } - rc = CheckAuthKeysLine(current, currentSz, key, keySz); + rc = CheckAuthKeysLine(current, currentSz, key, keySz, isCert); if (rc == WSSHD_AUTH_SUCCESS) { foundKey = 1; break; @@ -1514,7 +1539,8 @@ WOLFSSHD_STATIC int SearchForPubKey(const char* path, if (ret == WSSHD_AUTH_SUCCESS) { ret = SearchKeysFile(authKeysPath, pubKeyCtx->publicKey, - pubKeyCtx->publicKeySz, uid, strictModes); + pubKeyCtx->publicKeySz, uid, strictModes, + pubKeyCtx->isCert); } return ret; @@ -1571,7 +1597,7 @@ static int OsshCertCheckPrincipal(const WS_UserAuthData_PublicKey* pubKeyCtx, nameSz = (word32)WSTRLEN(name); while (idx + UINT32_SZ <= sz) { - ato32(p + idx, &entSz); + entSz = AuthReadU32(p + idx); idx += UINT32_SZ; if (entSz > sz - idx) { break; /* malformed principals region */ @@ -1846,7 +1872,8 @@ static int CheckPublicKeyUnix(const char* name, * anchor, so it is always secure-gated, regardless of StrictModes. */ if (ret == WSSHD_AUTH_SUCCESS) { ret = SearchKeysFile(usrCaKeysFile, pubKeyCtx->caKey, - pubKeyCtx->caKeySz, geteuid(), 1 /* strictModes */); + pubKeyCtx->caKeySz, geteuid(), 1 /* strictModes */, + 0 /* isCert: caKey is a wire-format key, not a cert */); } /* Bind the certificate to the requested user via its principals. */ diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 767f1e949..7e2cc87c2 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -170,8 +170,9 @@ void DoFakePasswordCheck(WS_UserAuthData* authData); void wolfSSHD_ResetFakePasswordCheckCountForTest(void); int wolfSSHD_GetFakePasswordCheckCountForTest(void); #endif +/* isCert: skips wire-format cross-check for DER certs. */ int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, - word32 keySz); + word32 keySz, int isCert); int ResolveAuthKeysPath(const char* homeDir, const char* pattern, const char* user, char* resolved); int CAKeysFileDiffers(const char* a, const char* b); diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 5953cf46f..ed6958e27 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -2887,6 +2887,25 @@ static int test_ScanShadowFile_truncatedLine(void) #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ #ifdef WOLFSSL_BASE64_ENCODE +/* Build SSH wire-format public key blob. Returns size or 0 if too small. */ +static word32 BuildWireKeyBlob(const char* type, const byte* payload, + word32 payloadSz, byte* out, word32 outSz) +{ + word32 typeLen = (word32)WSTRLEN(type); + word32 blobSz = LENGTH_SZ + typeLen + payloadSz; + + if (outSz < blobSz) { + return 0; + } + out[0] = (byte)((typeLen >> 24) & 0xff); + out[1] = (byte)((typeLen >> 16) & 0xff); + out[2] = (byte)((typeLen >> 8) & 0xff); + out[3] = (byte)(typeLen & 0xff); + WMEMCPY(out + LENGTH_SZ, type, typeLen); + WMEMCPY(out + LENGTH_SZ + typeLen, payload, payloadSz); + return blobSz; +} + /* Build a mutable " " line; WSTRTOK mutates in place. */ static int BuildAuthKeysLineType(const char* type, const byte* key, word32 keySz, char* lineOut, word32 lineOutSz) @@ -2919,8 +2938,7 @@ static int BuildAuthKeysLine(const byte* key, word32 keySz, return BuildAuthKeysLineType("ssh-rsa", key, keySz, lineOut, lineOutSz); } -/* Confirms every key-type CheckAuthKeysLine accepts via wolfSSH_QueryKey() - * is recognized. */ +/* Test CheckAuthKeysLine type cross-check and rejection. */ static int test_CheckAuthKeysLineTypes(void) { static const char* types[] = { @@ -3001,6 +3019,9 @@ static int test_CheckAuthKeysLineTypes(void) static const char keyAStr[] = "wolfssh-auth-key-test-A-AAAAAAA"; const byte* keyA = (const byte*)keyAStr; const word32 keySz = (word32)(sizeof(keyAStr) - 1); + /* 64 covers the longest type name. */ + byte blob[LENGTH_SZ + 64 + sizeof(keyAStr) - 1]; + word32 blobSz; char line[256]; char lineCopy[256]; word32 i; @@ -3008,7 +3029,14 @@ static int test_CheckAuthKeysLineTypes(void) int rc; for (i = 0; i < (word32)(sizeof(types) / sizeof(types[0])); i++) { - ret = BuildAuthKeysLineType(types[i], keyA, keySz, line, sizeof(line)); + blobSz = BuildWireKeyBlob(types[i], keyA, keySz, blob, sizeof(blob)); + if (blobSz == 0) { + Log(" CheckAuthKeysLine type %s: blob build failed.\n", + types[i]); + return WS_BUFFER_E; + } + ret = BuildAuthKeysLineType(types[i], blob, blobSz, line, + sizeof(line)); if (ret != WS_SUCCESS) { Log(" CheckAuthKeysLine type %s: build failed.\n", types[i]); return ret; @@ -3016,10 +3044,9 @@ static int test_CheckAuthKeysLineTypes(void) Log(" Testing scenario: known type %s reaches key comparison.", types[i]); WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); - /* Matching key: a recognized type must proceed to the key - * comparison and report success. */ + /* Valid token proceeds to key comparison. */ rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), - keyA, keySz); + blob, blobSz, 0); if (rc == WSSHD_AUTH_SUCCESS) { Log(" PASSED.\n"); } @@ -3029,16 +3056,20 @@ static int test_CheckAuthKeysLineTypes(void) } } - /* An unknown type must be skipped (not matched) rather than aborting - * the whole authorized_keys scan with a fatal error. */ - ret = BuildAuthKeysLineType("ssh-bogus-type", keyA, keySz, line, + /* Reject line token naming a different key type. */ + blobSz = BuildWireKeyBlob("ssh-rsa", keyA, keySz, blob, sizeof(blob)); + if (blobSz == 0) { + return WS_BUFFER_E; + } + ret = BuildAuthKeysLineType("ssh-bogus-type", blob, blobSz, line, sizeof(line)); if (ret != WS_SUCCESS) { return ret; } - Log(" Testing scenario: unknown type is rejected."); + Log(" Testing scenario: mismatched line type is rejected."); WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); - rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), keyA, keySz); + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), blob, blobSz, + 0); if (rc == WSSHD_AUTH_FAILURE) { Log(" PASSED.\n"); } @@ -3078,10 +3109,17 @@ static int CheckAuthKeysLineMaxSzCase(const char* type, word32 keySz, } if (ret == WS_SUCCESS) { + word32 typeLen = (word32)WSTRLEN(type); + word32 headerSz = LENGTH_SZ + typeLen; word32 i; - /* Non-repeating pattern so a truncation bug shows up as a - * mismatch, not accidental luck. */ - for (i = 0; i < keySz; i++) { + + /* Provide real header; the rest is non-repeating filler. */ + key[0] = (byte)((typeLen >> 24) & 0xff); + key[1] = (byte)((typeLen >> 16) & 0xff); + key[2] = (byte)((typeLen >> 8) & 0xff); + key[3] = (byte)(typeLen & 0xff); + WMEMCPY(key + LENGTH_SZ, type, typeLen); + for (i = headerSz; i < keySz; i++) { key[i] = (byte)(i * 31 + 7); } @@ -3101,7 +3139,7 @@ static int CheckAuthKeysLineMaxSzCase(const char* type, word32 keySz, } else { WMEMCPY(lineCopy, line, lineLen + 1); - rc = CheckAuthKeysLine(lineCopy, lineLen, key, keySz); + rc = CheckAuthKeysLine(lineCopy, lineLen, key, keySz, 0); if (rc == WSSHD_AUTH_SUCCESS) { Log(" PASSED.\n"); } @@ -3186,22 +3224,30 @@ static int test_CheckAuthKeysLineMaxSz(void) static int test_CheckAuthKeysLine(void) { int ret = WS_SUCCESS; - /* keyALastByte differs from keyA only in the final byte, killing a - * dropped-ConstantCompare mutation that the length check alone would miss. */ + /* blobALastByte differs in final byte to test ConstantCompare. */ static const char keyAStr[] = "wolfssh-auth-key-test-A-AAAAAAA"; static const char keyBStr[] = "wolfssh-auth-key-test-B-BBBBBBB"; const byte* keyA = (const byte*)keyAStr; const byte* keyB = (const byte*)keyBStr; const word32 keySz = (word32)(sizeof(keyAStr) - 1); - byte keyALastByte[sizeof(keyAStr) - 1]; + byte blobA[LENGTH_SZ + 7 /* strlen("ssh-rsa") */ + sizeof(keyAStr) - 1]; + byte blobB[sizeof(blobA)]; + byte blobALastByte[sizeof(blobA)]; + word32 blobSz; char line[256]; - char lineCopy[320]; /* fits the longer unsupported-type scenario line */ + char lineCopy[320]; /* fits the longer mismatched-type scenario line */ int rc; - WMEMCPY(keyALastByte, keyA, keySz); - keyALastByte[keySz - 1] ^= 0x01; + blobSz = BuildWireKeyBlob("ssh-rsa", keyA, keySz, blobA, sizeof(blobA)); + if (blobSz == 0 || + BuildWireKeyBlob("ssh-rsa", keyB, keySz, blobB, sizeof(blobB)) + != blobSz) { + return WS_BUFFER_E; + } + WMEMCPY(blobALastByte, blobA, blobSz); + blobALastByte[blobSz - 1] ^= 0x01; - ret = BuildAuthKeysLine(keyA, keySz, line, sizeof(line)); + ret = BuildAuthKeysLine(blobA, blobSz, line, sizeof(line)); if (ret != WS_SUCCESS) { return ret; } @@ -3209,7 +3255,7 @@ static int test_CheckAuthKeysLine(void) Log(" Testing scenario: matching key authenticates."); WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), - keyA, keySz); + blobA, blobSz, 0); if (rc == WSSHD_AUTH_SUCCESS) { Log(" PASSED.\n"); } @@ -3222,7 +3268,7 @@ static int test_CheckAuthKeysLine(void) Log(" Testing scenario: different same-length key is rejected."); WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), - keyB, keySz); + blobB, blobSz, 0); if (rc == WSSHD_AUTH_FAILURE) { Log(" PASSED.\n"); } @@ -3237,7 +3283,7 @@ static int test_CheckAuthKeysLine(void) "rejected."); WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), - keyALastByte, keySz); + blobALastByte, blobSz, 0); if (rc == WSSHD_AUTH_FAILURE) { Log(" PASSED.\n"); } @@ -3248,14 +3294,117 @@ static int test_CheckAuthKeysLine(void) } if (ret == WS_SUCCESS) { - /* An unsupported key type must be skipped (WSSHD_AUTH_FAILURE), not - * treated as a hard error, so SearchKeysFile keeps scanning later - * TrustedUserCAKeys entries. */ - Log(" Testing scenario: unsupported key type is skipped."); + /* Mismatched line type token is rejected safely. */ + Log(" Testing scenario: mismatched line type is rejected."); WSNPRINTF(lineCopy, sizeof(lineCopy), "sk-ssh-ed25519@openssh.com %s", line + WSTRLEN("ssh-rsa ")); rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), - keyA, keySz); + blobA, blobSz, 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + /* OpenSSH maps an RSA SHA-2 signature name onto ssh-rsa, so these + * lines authenticate an ssh-rsa key there and must here too. */ + static const char* rsaAliases[] = { "rsa-sha2-256", "rsa-sha2-512" }; + word32 i; + + for (i = 0; i < (word32)(sizeof(rsaAliases) / sizeof(rsaAliases[0])); + i++) { + Log(" Testing scenario: %s line type for an ssh-rsa key " + "authenticates.", rsaAliases[i]); + ret = BuildAuthKeysLineType(rsaAliases[i], blobA, blobSz, line, + sizeof(line)); + if (ret != WS_SUCCESS) { + return ret; + } + WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), + blobA, blobSz, 0); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + break; + } + } + } + + if (ret == WS_SUCCESS) { + /* A name that only looks like an alias must still be rejected, so the + * mapping can't decay into a prefix match. */ + Log(" Testing scenario: rsa-sha2-384 line type is rejected."); + ret = BuildAuthKeysLineType("rsa-sha2-384", blobA, blobSz, line, + sizeof(line)); + if (ret != WS_SUCCESS) { + return ret; + } + WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), + blobA, blobSz, 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + /* Type and embedded blob type both agree on a cert type name, so + * this isolates the cert-type guard from the type/blob match. */ + static const char certType[] = "ssh-rsa-cert-v01@openssh.com"; + byte certBlob[LENGTH_SZ + sizeof(certType) - 1 + sizeof(keyAStr) - 1]; + word32 certBlobSz; + + certBlobSz = BuildWireKeyBlob(certType, keyA, keySz, certBlob, + sizeof(certBlob)); + if (certBlobSz == 0) { + return WS_BUFFER_E; + } + ret = BuildAuthKeysLineType(certType, certBlob, certBlobSz, line, + sizeof(line)); + if (ret != WS_SUCCESS) { + return ret; + } + Log(" Testing scenario: OpenSSH cert-type line is rejected."); + WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), + certBlob, certBlobSz, 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + /* Blob too short to hold the wire-format length field + * (keyCandSz >= 4 else-branch in auth.c). */ + static const byte shortBlob[] = { 0x00, 0x01 }; + + Log(" Testing scenario: short blob (< 4 bytes) is rejected."); + ret = BuildAuthKeysLineType("ssh-rsa", shortBlob, sizeof(shortBlob), + line, sizeof(line)); + if (ret != WS_SUCCESS) { + return ret; + } + WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); + /* Pass shortBlob itself as the key, not blobA: otherwise the + * pre-existing keyCandSz != keySz check masks the guard. */ + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), + shortBlob, sizeof(shortBlob), 0); if (rc == WSSHD_AUTH_FAILURE) { Log(" PASSED.\n"); } @@ -3265,6 +3414,58 @@ static int test_CheckAuthKeysLine(void) } } + if (ret == WS_SUCCESS) { + /* truncType is not RSA-SHA2-aliased, so keyTypeSz equals the + * declared type length (7) and the XMEMCMP content matches: only + * the typeStrSz > keyCandSz - 4 bound can reject this blob. */ + static const char truncType[] = "ssh-rsa"; + static const byte truncatedBlob[] = { + 0x00, 0x00, 0x00, 0x07, 's', 's', 'h' + }; + + Log(" Testing scenario: truncated blob (declared type length " + "exceeds decoded blob) is rejected."); + ret = BuildAuthKeysLineType(truncType, truncatedBlob, + sizeof(truncatedBlob), line, sizeof(line)); + if (ret != WS_SUCCESS) { + return ret; + } + WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); + /* Pass truncatedBlob itself as the key, not blobA: otherwise the + * pre-existing keyCandSz != keySz check masks the guard. */ + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), + truncatedBlob, sizeof(truncatedBlob), 0); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + /* isCert: key is a raw DER certificate, not an SSH wire-format + * blob, so the type/embedded-type cross-check must be skipped. */ + Log(" Testing scenario: cert candidate with non-wire-format " + "payload authenticates."); + ret = BuildAuthKeysLineType("ssh-rsa", keyA, keySz, line, + sizeof(line)); + if (ret != WS_SUCCESS) { + return ret; + } + WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), + keyA, keySz, 1 /* isCert */); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + return ret; } @@ -3280,6 +3481,11 @@ static int test_SearchForPubKey(void) const byte* keyA = (const byte*)keyAStr; const byte* keyB = (const byte*)keyBStr; const word32 keySz = (word32)(sizeof(keyAStr) - 1); + /* CheckAuthKeysLine cross-checks the type token against the embedded type, + * so pubKeyCtx.publicKey needs a full SSH wire-format key blob. */ + byte blobA[LENGTH_SZ + 7 /* strlen("ssh-rsa") */ + sizeof(keyAStr) - 1]; + byte blobB[sizeof(blobA)]; + word32 blobSz; char base[] = "/tmp/wolfsshd_pkXXXXXX"; char keysPath[64] = ""; char missPath[64] = ""; @@ -3289,6 +3495,13 @@ static int test_SearchForPubKey(void) FILE* f = NULL; int rc; + blobSz = BuildWireKeyBlob("ssh-rsa", keyA, keySz, blobA, sizeof(blobA)); + if (blobSz == 0 || + BuildWireKeyBlob("ssh-rsa", keyB, keySz, blobB, sizeof(blobB)) + != blobSz) { + return WS_BUFFER_E; + } + if (mkdtemp(base) == NULL) { Log(" mkdtemp failed.\n"); ret = WS_FATAL_ERROR; @@ -3297,7 +3510,7 @@ static int test_SearchForPubKey(void) if (ret == WS_SUCCESS) { snprintf(keysPath, sizeof(keysPath), "%s/authorized_keys", base); snprintf(missPath, sizeof(missPath), "%s/absent_keys", base); - ret = BuildAuthKeysLine(keyA, keySz, line, sizeof(line)); + ret = BuildAuthKeysLine(blobA, blobSz, line, sizeof(line)); } if (ret == WS_SUCCESS) { @@ -3321,12 +3534,12 @@ static int test_SearchForPubKey(void) } WMEMSET(&pubKeyCtx, 0, sizeof(pubKeyCtx)); - pubKeyCtx.publicKeySz = keySz; + pubKeyCtx.publicKeySz = blobSz; /* StrictModes disabled so the check stays hermetic (no ownership gate). */ if (ret == WS_SUCCESS) { Log(" Testing scenario: authorized key is accepted."); - pubKeyCtx.publicKey = keyA; + pubKeyCtx.publicKey = blobA; rc = SearchForPubKey(base, keysPath, "testuser", &pubKeyCtx, uid, 0); if (rc == WSSHD_AUTH_SUCCESS) { Log(" PASSED.\n"); @@ -3341,7 +3554,7 @@ static int test_SearchForPubKey(void) * secure-open branch must also accept the authorized key. */ if (ret == WS_SUCCESS) { Log(" Testing scenario: authorized key accepted under StrictModes."); - pubKeyCtx.publicKey = keyA; + pubKeyCtx.publicKey = blobA; rc = SearchForPubKey(base, keysPath, "testuser", &pubKeyCtx, uid, 1); if (rc == WSSHD_AUTH_SUCCESS) { Log(" PASSED.\n"); @@ -3354,7 +3567,7 @@ static int test_SearchForPubKey(void) if (ret == WS_SUCCESS) { Log(" Testing scenario: unauthorized key is rejected."); - pubKeyCtx.publicKey = keyB; + pubKeyCtx.publicKey = blobB; rc = SearchForPubKey(base, keysPath, "testuser", &pubKeyCtx, uid, 0); if (rc == WSSHD_AUTH_FAILURE) { Log(" PASSED.\n"); @@ -3365,10 +3578,75 @@ static int test_SearchForPubKey(void) } } + /* Malformed line (bad base64) must be skipped, not abort the scan. */ + if (ret == WS_SUCCESS) { + f = fopen(keysPath, "w"); + if (f == NULL) { + Log(" fopen of authorized_keys failed.\n"); + ret = WS_FATAL_ERROR; + } + else { + fputs("no-pty not-base64!!\n", f); + fputs(line, f); + fputs("\n", f); + fclose(f); + } + } + if (ret == WS_SUCCESS && chmod(keysPath, S_IRUSR | S_IWUSR) != 0) { + Log(" chmod of authorized_keys failed.\n"); + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { + Log(" Testing scenario: malformed line is skipped, scan " + "continues to matching key."); + pubKeyCtx.publicKey = blobA; + rc = SearchForPubKey(base, keysPath, "testuser", &pubKeyCtx, uid, 0); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + + /* Malformed line (single token, no key field) must be skipped too, + * not abort the scan. */ + if (ret == WS_SUCCESS) { + f = fopen(keysPath, "w"); + if (f == NULL) { + Log(" fopen of authorized_keys failed.\n"); + ret = WS_FATAL_ERROR; + } + else { + fputs("single-token-line\n", f); + fputs(line, f); + fputs("\n", f); + fclose(f); + } + } + if (ret == WS_SUCCESS && chmod(keysPath, S_IRUSR | S_IWUSR) != 0) { + Log(" chmod of authorized_keys failed.\n"); + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { + Log(" Testing scenario: single-token line is skipped, scan " + "continues to matching key."); + pubKeyCtx.publicKey = blobA; + rc = SearchForPubKey(base, keysPath, "testuser", &pubKeyCtx, uid, 0); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + /* A missing authorized_keys file is an error, not a silent accept. */ if (ret == WS_SUCCESS) { Log(" Testing scenario: missing keys file returns an error."); - pubKeyCtx.publicKey = keyA; + pubKeyCtx.publicKey = blobA; rc = SearchForPubKey(base, missPath, "testuser", &pubKeyCtx, uid, 0); if (rc < 0) { Log(" PASSED.\n"); @@ -5964,14 +6242,16 @@ static int test_CheckPublicKeyUnixOrdering(void) word64 now = (word64)WTIME(NULL); FILE* f = NULL; WOLFSSHD_AUTH* auth = NULL; - /* Arbitrary bytes standing in for a CA signing-key blob; only byte-for-byte - * trust-list membership is checked here, not a signature. */ - static const byte caBlob[32] = { + /* Arbitrary CA signing-key blob for byte-for-byte trust-list check. + * Wrapped as a full SSH wire-format blob for type token cross-check. */ + static const byte caPayload[32] = { 0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, 0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10, 0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18, 0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x20 }; + byte caBlob[LENGTH_SZ + 7 /* strlen("ssh-rsa") */ + sizeof(caPayload)]; + word32 caBlobSz; /* principals list that does not contain the running account. */ static const byte wrongP[] = { 0,0,0,7, 'n','o','m','a','t','c','h' }; @@ -5980,6 +6260,12 @@ static int test_CheckPublicKeyUnixOrdering(void) Log(" getpwuid failed.\n"); return WS_FATAL_ERROR; } + caBlobSz = BuildWireKeyBlob("ssh-rsa", caPayload, sizeof(caPayload), + caBlob, sizeof(caBlob)); + if (caBlobSz == 0) { + return WS_BUFFER_E; + } + user = pw->pw_name; nameLen = (word32)WSTRLEN(user); if (nameLen == 0 || nameLen + 4 > (word32)sizeof(principals)) { @@ -6008,7 +6294,7 @@ static int test_CheckPublicKeyUnixOrdering(void) snprintf(emptyFile, sizeof(emptyFile), "%s/empty", base); /* A TrustedUserCAKeys file that trusts caBlob, plus an empty one. */ - ret = BuildAuthKeysLine(caBlob, (word32)sizeof(caBlob), line, sizeof(line)); + ret = BuildAuthKeysLine(caBlob, caBlobSz, line, sizeof(line)); if (ret == WS_SUCCESS) { f = fopen(caFile, "w"); if (f == NULL) { @@ -6041,7 +6327,7 @@ static int test_CheckPublicKeyUnixOrdering(void) WMEMSET(&good, 0, sizeof(good)); good.isOsshCert = 1; good.caKey = caBlob; - good.caKeySz = (word32)sizeof(caBlob); + good.caKeySz = caBlobSz; good.principals = principals; good.principalsSz = pSz; good.validAfter = 0; diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 8f1514057..6ab963265 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -3109,8 +3109,16 @@ static int StartSSHD(int argc, char** argv) } else { unsigned int z; + + /* Zero first: _freeWinArgs() walks all argc slots. */ + WMEMSET(argv, 0, argc * sizeof(char*)); for (z = 0; z < argc; z++) { argv[z] = _convertHelper(cmdArgs[z], NULL); + if (argv[z] == NULL) { + /* mygetopt() dereferences every entry it walks. */ + ret = WS_MEMORY_E; + break; + } } } } diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 8da868b33..00320b7c8 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -2051,9 +2051,10 @@ typedef struct { const char* label; } MlDsaCompositeEntry; -/* NULL-terminated so the table is never empty if ECDSA and Ed25519/Ed448 - * are both disabled */ +/* NULL-terminated so the table is never empty if composites are compiled + * out or ECDSA and Ed25519/Ed448 are both disabled */ static const MlDsaCompositeEntry mldsaCompositeEntries[] = { +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ !defined(NO_SHA512) { "mldsa44-ed25519", "./keys/server-key-mldsa44ed25519", @@ -2082,6 +2083,7 @@ static const MlDsaCompositeEntry mldsaCompositeEntries[] = { { "mldsa87-es384", "./keys/server-key-mldsa87es384", "ML-DSA-87+ES384" }, #endif +#endif /* !WOLFSSH_NO_MLDSA_COMPOSITES */ { NULL, NULL, NULL } }; diff --git a/src/internal.c b/src/internal.c index 21d667507..be64b1d9d 100644 --- a/src/internal.c +++ b/src/internal.c @@ -64,18 +64,23 @@ #ifndef WOLFSSH_NO_MLDSA #include - /* SendKexGetSigningKey() bitwise-copies MlDsaKey, so it must have no - * heap-allocated/self-referential members; guard against that here. */ - #if defined(WOLFSSL_MLDSA_DYNAMIC_KEYS) || defined(WOLFSSL_DILITHIUM_DYNAMIC_KEYS) || \ - (!(defined(WC_MLDSA_FIXED_ARRAY) || defined(WC_DILITHIUM_FIXED_ARRAY)) && \ - (defined(WC_MLDSA_CACHE_MATRIX_A) || defined(WC_DILITHIUM_CACHE_MATRIX_A) || \ - defined(WC_MLDSA_CACHE_PRIV_VECTORS) || defined(WC_DILITHIUM_CACHE_PRIV_VECTORS) || \ - defined(WC_MLDSA_CACHE_PUB_VECTORS) || defined(WC_DILITHIUM_CACHE_PUB_VECTORS))) - #error "wolfSSH's ML-DSA composite key handling assumes MlDsaKey " \ - "is flat and safe to bitwise-copy; this wolfCrypt build " \ - "config gives it heap-allocated/pointer members, so " \ - "SendKexGetSigningKey() must be reworked before it can be " \ - "used with WOLFSSH_NO_MLDSA unset." + #ifndef WOLFSSH_NO_MLDSA_COMPOSITES + /* SendKexGetSigningKey() bitwise-copies MlDsaKey, so it must be + * flat. */ + #if defined(WOLFSSL_MLDSA_DYNAMIC_KEYS) || \ + defined(WOLFSSL_DILITHIUM_DYNAMIC_KEYS) || \ + (!(defined(WC_MLDSA_FIXED_ARRAY) || \ + defined(WC_DILITHIUM_FIXED_ARRAY)) && \ + (defined(WC_MLDSA_CACHE_MATRIX_A) || \ + defined(WC_DILITHIUM_CACHE_MATRIX_A) || \ + defined(WC_MLDSA_CACHE_PRIV_VECTORS) || \ + defined(WC_DILITHIUM_CACHE_PRIV_VECTORS) || \ + defined(WC_MLDSA_CACHE_PUB_VECTORS) || \ + defined(WC_DILITHIUM_CACHE_PUB_VECTORS))) + #error "MlDsaKey must be flat for bitwise copy. Disable " \ + "dynamic keys/caching in wolfCrypt, or define " \ + "WOLFSSH_NO_MLDSA_COMPOSITES." + #endif #endif #endif @@ -1065,6 +1070,7 @@ static const char cannedKexAlgoNames[] = /* ML-DSA listed first (post-quantum priority), then ECDSA, ED25519, RSA. */ static const char cannedKeyAlgoNames[] = +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) "ssh-mldsa87-ed448@wolfssl.com," #endif @@ -1087,6 +1093,7 @@ static const char cannedKeyAlgoNames[] = #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) "ssh-mldsa44-es256@wolfssl.com," #endif +#endif /* !WOLFSSH_NO_MLDSA_COMPOSITES */ #ifndef WOLFSSH_NO_MLDSA87 "ssh-mldsa-87," #endif @@ -1166,6 +1173,7 @@ static const char cannedKeyAlgoNames[] = * ("*-cert-v01@openssh.com") names: host-cert verification is unimplemented, so * a client must not advertise them as host keys. Keep plain/X.509 in sync. */ static const char cannedKeyAlgoNamesHostKey[] = +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) "ssh-mldsa87-ed448@wolfssl.com," #endif @@ -1188,6 +1196,7 @@ static const char cannedKeyAlgoNamesHostKey[] = #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) "ssh-mldsa44-es256@wolfssl.com," #endif +#endif /* !WOLFSSH_NO_MLDSA_COMPOSITES */ #ifndef WOLFSSH_NO_MLDSA87 "ssh-mldsa-87," #endif @@ -3492,6 +3501,7 @@ static const NameIdPair NameIdMap[] = { #ifndef WOLFSSH_NO_MLDSA44 { ID_MLDSA44, TYPE_KEY, "ssh-mldsa-44" }, #endif +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) { ID_MLDSA44_ES256, TYPE_KEY, "ssh-mldsa44-es256@wolfssl.com" }, #endif @@ -3514,6 +3524,7 @@ static const NameIdPair NameIdMap[] = { #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) { ID_MLDSA87_ED448, TYPE_KEY, "ssh-mldsa87-ed448@wolfssl.com" }, #endif +#endif /* !WOLFSSH_NO_MLDSA_COMPOSITES */ #ifndef WOLFSSH_NO_MLDSA65 { ID_MLDSA65, TYPE_KEY, "ssh-mldsa-65" }, #endif @@ -5451,6 +5462,7 @@ static const byte cannedKeyAlgoClient[] = { #endif /* WOLFSSH_NO_SSH_RSA_SHA1 */ #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ #endif /* WOLFSSH_CERTS */ +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) ID_MLDSA87_ED448, #endif @@ -5473,6 +5485,7 @@ static const byte cannedKeyAlgoClient[] = { #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) ID_MLDSA44_ES256, #endif +#endif /* !WOLFSSH_NO_MLDSA_COMPOSITES */ #ifndef WOLFSSH_NO_MLDSA87 ID_MLDSA87, #endif @@ -6710,16 +6723,6 @@ struct wolfSSH_sigKeyBlock { #ifndef WOLFSSH_NO_MLDSA struct { WS_MlDsaCompositeBody base; - /* largest mldsaPubSz + tradPubSz across WS_GetCompositeParams() - * combos; keep in sync with any new combo added there */ -#ifndef WOLFSSH_NO_MLDSA87 - byte q[WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; -#elif !defined(WOLFSSH_NO_MLDSA65) - byte q[WC_MLDSA_65_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; -#else - byte q[WC_MLDSA_44_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; -#endif - word32 qSz; } mldsa_composite; #endif } sk; @@ -14473,27 +14476,16 @@ struct wolfSSH_sigKeyBlockFull { struct { MlDsaKey key; /* Size to highest enabled ML-DSA level; qSz tracks actual. */ -#ifndef WOLFSSH_NO_MLDSA87 - byte q[WC_MLDSA_87_PUB_KEY_SIZE]; -#elif !defined(WOLFSSH_NO_MLDSA65) - byte q[WC_MLDSA_65_PUB_KEY_SIZE]; -#else - byte q[WC_MLDSA_44_PUB_KEY_SIZE]; -#endif + byte q[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ]; word32 qSz; } mldsa; struct { WS_MlDsaCompositeBody base; byte tradInit; /* largest mldsaPubSz + tradPubSz across - * WS_GetCompositeParams() combos; keep in sync */ -#ifndef WOLFSSH_NO_MLDSA87 - byte q[WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; -#elif !defined(WOLFSSH_NO_MLDSA65) - byte q[WC_MLDSA_65_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; -#else - byte q[WC_MLDSA_44_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; -#endif + * WS_GetCompositeParams() combos */ + byte q[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ + + COMPOSITE_MAX_TRAD_PUB_SZ]; word32 qSz; } mldsa_composite; #endif @@ -16320,6 +16312,39 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, #endif /* WOLFSSH_NO_RSA */ +#ifndef WOLFSSH_NO_ECDSA +/* Encode ECDSA r/s mpints to [uint32 len][val] pairs, padding if needed. + * Returns WS_BUFFER_E when outSz can't hold both. */ +static int EncodeEcdsaRsToMpints(byte* out, word32 outSz, + const byte* r, word32 rSz, byte rPad, + const byte* s, word32 sSz, byte sPad, word32* written) +{ + word32 idx = 0; + + if (outSz < (2 * LENGTH_SZ) + rSz + rPad + sSz + sPad) { + return WS_BUFFER_E; + } + + c32toa(rSz + rPad, out + idx); + idx += LENGTH_SZ; + if (rPad) + out[idx++] = 0; + WMEMCPY(out + idx, r, rSz); + idx += rSz; + + c32toa(sSz + sPad, out + idx); + idx += LENGTH_SZ; + if (sPad) + out[idx++] = 0; + WMEMCPY(out + idx, s, sSz); + idx += sSz; + + *written = idx; + + return WS_SUCCESS; +} +#endif /* !WOLFSSH_NO_ECDSA */ + static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, struct wolfSSH_sigKeyBlockFull *sigKey) #ifndef WOLFSSH_NO_ECDSA @@ -16334,6 +16359,8 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, enum wc_HashType hashId; word32 rSz = MAX_ECC_BYTES + ECC_MAX_PAD_SZ, sSz = MAX_ECC_BYTES + ECC_MAX_PAD_SZ; + /* Save capacity since mpint form can be longer than ASN.1 form. */ + word32 sigCap = *sigSz; byte rPad, sPad; #ifndef WOLFSSH_SMALL_STACK byte r_s[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; @@ -16428,22 +16455,18 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, } if (ret == WS_SUCCESS) { - int idx = 0; + word32 written; + rPad = (r[0] & 0x80) ? 1 : 0; sPad = (s[0] & 0x80) ? 1 : 0; - *sigSz = (LENGTH_SZ * 2) + rSz + rPad + sSz + sPad; - - c32toa(rSz + rPad, sig + idx); - idx += LENGTH_SZ; - if (rPad) - sig[idx++] = 0; - WMEMCPY(sig + idx, r, rSz); - idx += rSz; - c32toa(sSz + sPad, sig + idx); - idx += LENGTH_SZ; - if (sPad) - sig[idx++] = 0; - WMEMCPY(sig + idx, s, sSz); + ret = EncodeEcdsaRsToMpints(sig, sigCap, r, rSz, rPad, s, sSz, + sPad, &written); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SignHEcdsa: sig buffer too small for mpints"); + } + else { + *sigSz = written; + } } WS_FORCEZERO(digest, sizeof(digest)); @@ -19046,8 +19069,10 @@ static int PrepareUserAuthRequestEcc(WOLFSSH* ssh, word32* payloadSz, } +/* outputSz bounds the variable-length r/s mpint encoding; the fixed-size + * builders (RSA, Ed25519, ML-DSA) fit PreparePacket()'s estimate unbounded. */ static int BuildUserAuthRequestEcc(WOLFSSH* ssh, - byte* output, word32* idx, + byte* output, word32 outputSz, word32* idx, const WS_UserAuthData* authData, const byte* sigStart, word32 sigStartIdx, WS_KeySignature* keySig) @@ -19119,6 +19144,13 @@ static int BuildUserAuthRequestEcc(WOLFSSH* ssh, sig_ptr, &sigSz, authData->sf.publicKey.publicKey, authData->sf.publicKey.publicKeySz, 0); + if (ret == WS_SUCCESS) { + /* begin indexes into output, whose capacity is outputSz. */ + if (outputSz <= begin || outputSz - begin < LENGTH_SZ + sigSz) { + WLOG(WS_LOG_DEBUG, "SUAR: ECDSA agent sig doesn't fit output"); + ret = WS_BUFFER_E; + } + } if (ret == WS_SUCCESS) { c32toa(sigSz, output + begin); begin += LENGTH_SZ; @@ -19192,8 +19224,22 @@ static int BuildUserAuthRequestEcc(WOLFSSH* ssh, } if (ret == WS_SUCCESS) { + word32 outRemaining; + namesSz = (word32)WSTRLEN(names); + /* Bound the whole block, not just the mpints. */ + outRemaining = (outputSz > begin) ? outputSz - begin : 0; + if (outRemaining < namesSz + (LENGTH_SZ * 5) + + rSz + rPad + sSz + sPad) { + WLOG(WS_LOG_DEBUG, "SUAR: ECDSA sig doesn't fit output"); + ret = WS_BUFFER_E; + } + } + + if (ret == WS_SUCCESS) { + word32 written = 0; + c32toa(rSz + rPad + sSz + sPad + namesSz + LENGTH_SZ * 4, output + begin); begin += LENGTH_SZ; @@ -19207,23 +19253,12 @@ static int BuildUserAuthRequestEcc(WOLFSSH* ssh, c32toa(rSz + rPad + sSz + sPad + LENGTH_SZ * 2, output + begin); begin += LENGTH_SZ; - c32toa(rSz + rPad, output + begin); - begin += LENGTH_SZ; - - if (rPad) - output[begin++] = 0; - - WMEMCPY(output + begin, r_ptr, rSz); - begin += rSz; - - c32toa(sSz + sPad, output + begin); - begin += LENGTH_SZ; - - if (sPad) - output[begin++] = 0; - - WMEMCPY(output + begin, s_ptr, sSz); - begin += sSz; + ret = EncodeEcdsaRsToMpints(output + begin, + outputSz - begin, r_ptr, rSz, rPad, s_ptr, sSz, sPad, + &written); + if (ret == WS_SUCCESS) { + begin += written; + } } } } @@ -19314,8 +19349,10 @@ static int PrepareUserAuthRequestEccCert(WOLFSSH* ssh, word32* payloadSz, } +/* outputSz bounds the variable-length r/s mpint encoding; the fixed-size + * builders (RSA, Ed25519, ML-DSA) fit PreparePacket()'s estimate unbounded. */ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, - byte* output, word32* idx, + byte* output, word32 outputSz, word32* idx, const WS_UserAuthData* authData, const byte* sigStart, word32 sigStartIdx, WS_KeySignature* keySig) @@ -19370,6 +19407,13 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, sig, &sigSz, authData->sf.publicKey.publicKey, authData->sf.publicKey.publicKeySz, 0); + if (ret == WS_SUCCESS) { + /* begin indexes into output, whose capacity is outputSz. */ + if (outputSz <= begin || outputSz - begin < LENGTH_SZ + sigSz) { + WLOG(WS_LOG_DEBUG, "SUAR: ECDSA agent sig doesn't fit output"); + ret = WS_BUFFER_E; + } + } if (ret == WS_SUCCESS) { c32toa(sigSz, output + begin); begin += LENGTH_SZ; @@ -19453,8 +19497,23 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, } if (ret == WS_SUCCESS) { + word32 outRemaining; + namesSz = (word32)WSTRLEN(names); + /* Bound the whole block, not just the mpints. */ + outRemaining = (outputSz > begin) ? outputSz - begin : 0; + if (outRemaining < namesSz + (LENGTH_SZ * 5) + + rSz + rPad + sSz + sPad) { + WLOG(WS_LOG_DEBUG, + "SUAR: ECDSA cert sig doesn't fit output"); + ret = WS_BUFFER_E; + } + } + + if (ret == WS_SUCCESS) { + word32 written = 0; + c32toa(rSz + rPad + sSz + sPad + namesSz+ LENGTH_SZ * 4, output + begin); begin += LENGTH_SZ; @@ -19468,23 +19527,12 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, c32toa(rSz + rPad + sSz + sPad + LENGTH_SZ * 2, output + begin); begin += LENGTH_SZ; - c32toa(rSz + rPad, output + begin); - begin += LENGTH_SZ; - - if (rPad) - output[begin++] = 0; - - WMEMCPY(output + begin, r, rSz); - begin += rSz; - - c32toa(sSz + sPad, output + begin); - begin += LENGTH_SZ; - - if (sPad) - output[begin++] = 0; - - WMEMCPY(output + begin, s, sSz); - begin += sSz; + ret = EncodeEcdsaRsToMpints(output + begin, + outputSz - begin, r, rSz, rPad, s, sSz, sPad, + &written); + if (ret == WS_SUCCESS) { + begin += written; + } } } } @@ -20172,7 +20220,8 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, begin += LENGTH_SZ; WMEMCPY(output + begin, pk->publicKey, pk->publicKeySz); begin += pk->publicKeySz; - ret = BuildUserAuthRequestEcc(ssh, output, &begin, + ret = BuildUserAuthRequestEcc(ssh, output, + ssh->outputBuffer.bufferSz, &begin, authData, sigStart, sigStartIdx, keySig); break; #ifdef WOLFSSH_CERTS @@ -20191,7 +20240,8 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, pk->publicKey, pk->publicKeySz, NULL, 0, output, &ssh->outputBuffer.bufferSz, &begin); if (ret == WS_SUCCESS) { - ret = BuildUserAuthRequestEccCert(ssh, output, &begin, + ret = BuildUserAuthRequestEccCert(ssh, output, + ssh->outputBuffer.bufferSz, &begin, authData, sigStart, sigStartIdx, keySig); } break; @@ -22619,6 +22669,10 @@ int WS_GetCompositeParams(byte keyId, CompositeParams* params) XMEMSET(params, 0, sizeof(*params)); params->keyId = keyId; +#ifdef WOLFSSH_NO_MLDSA_COMPOSITES + /* Fail unconditionally if composites disabled. */ + return WS_NOT_COMPILED; +#else switch (keyId) { #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) case ID_MLDSA44_ES256: @@ -22738,6 +22792,7 @@ int WS_GetCompositeParams(byte keyId, CompositeParams* params) } return WS_SUCCESS; +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ } int WS_Hash_Helper(enum wc_HashType hashId, const byte* msg, word32 msgSz, @@ -22776,6 +22831,14 @@ static void CompositeEccFree(void* key) wc_ecc_free((ecc_key*)key); } +/* returns 0 on success, negative on failure (wc_ecc_make_key_ex() code) */ +static int CompositeEccMakeKey(void* key, WC_RNG* rng, + const CompositeParams* params) +{ + return wc_ecc_make_key_ex(rng, (int)params->tradPrivSz, (ecc_key*)key, + params->eccCurveId); +} + /* returns 0 on success, negative on failure (wc_ecc_import_x963() code) */ static int CompositeEccImportPub(void* key, const byte* pub, word32 pubSz) { @@ -22862,31 +22925,17 @@ static int CompositeEccSign(void* key, WC_RNG* rng, void* heap, ret = wc_ecc_sig_to_rs(asnSig, asnSigSz, rBuf, &rSz, sBuf, &sSz); } if (ret == 0) { - word32 offset = 0; + /* RFC 5656 3.1.2: mpints with the top bit set need a zero pad. */ byte rPad = (rBuf[0] & 0x80) ? 1 : 0; byte sPad = (sBuf[0] & 0x80) ? 1 : 0; + word32 written; - /* RFC 5656 3.1.2: r/s are mpints; a positive value with its - * top bit set needs a leading zero pad byte. */ - if (*wireSigSz < (2U * LENGTH_SZ) + rSz + rPad + sSz + sPad) { + if (EncodeEcdsaRsToMpints(wireSig, *wireSigSz, rBuf, rSz, rPad, + sBuf, sSz, sPad, &written) != WS_SUCCESS) { ret = WS_BAD_ARGUMENT; } else { - c32toa(rSz + rPad, wireSig + offset); - offset += LENGTH_SZ; - if (rPad) - wireSig[offset++] = 0; - WMEMCPY(wireSig + offset, rBuf, rSz); - offset += rSz; - - c32toa(sSz + sPad, wireSig + offset); - offset += LENGTH_SZ; - if (sPad) - wireSig[offset++] = 0; - WMEMCPY(wireSig + offset, sBuf, sSz); - offset += sSz; - - *wireSigSz = offset; + *wireSigSz = written; } } #ifdef WOLFSSH_SMALL_STACK @@ -22980,7 +23029,7 @@ static int CompositeEccVerify(void* key, void* heap, } static const CompositeTradOps compositeEccOps = { - CompositeEccInit, CompositeEccFree, + CompositeEccInit, CompositeEccFree, CompositeEccMakeKey, CompositeEccImportPub, CompositeEccImportPriv, CompositeEccExportPrivOnly, CompositeEccExportPub, CompositeEccSign, CompositeEccVerify, @@ -23001,6 +23050,14 @@ static void CompositeEd25519Free(void* key) wc_ed25519_free((ed25519_key*)key); } +/* returns 0 on success, negative wc_ed25519_make_key() code on failure */ +static int CompositeEd25519MakeKey(void* key, WC_RNG* rng, + const CompositeParams* params) +{ + WOLFSSH_UNUSED(params); + return wc_ed25519_make_key(rng, ED25519_KEY_SIZE, (ed25519_key*)key); +} + /* returns 0 on success, negative wc_ed25519_import_public() code on * failure */ static int CompositeEd25519ImportPub(void* key, const byte* pub, word32 pubSz) @@ -23084,7 +23141,7 @@ static int CompositeEd25519Verify(void* key, void* heap, } static const CompositeTradOps compositeEd25519Ops = { - CompositeEd25519Init, CompositeEd25519Free, + CompositeEd25519Init, CompositeEd25519Free, CompositeEd25519MakeKey, CompositeEd25519ImportPub, CompositeEd25519ImportPriv, CompositeEd25519ExportPrivOnly, CompositeEd25519ExportPub, CompositeEd25519Sign, CompositeEd25519Verify, @@ -23105,6 +23162,14 @@ static void CompositeEd448Free(void* key) wc_ed448_free((ed448_key*)key); } +/* returns 0 on success, negative wc_ed448_make_key() code on failure */ +static int CompositeEd448MakeKey(void* key, WC_RNG* rng, + const CompositeParams* params) +{ + WOLFSSH_UNUSED(params); + return wc_ed448_make_key(rng, ED448_KEY_SIZE, (ed448_key*)key); +} + /* returns 0 on success, negative wc_ed448_import_public() code on failure */ static int CompositeEd448ImportPub(void* key, const byte* pub, word32 pubSz) { @@ -23185,7 +23250,7 @@ static int CompositeEd448Verify(void* key, void* heap, } static const CompositeTradOps compositeEd448Ops = { - CompositeEd448Init, CompositeEd448Free, + CompositeEd448Init, CompositeEd448Free, CompositeEd448MakeKey, CompositeEd448ImportPub, CompositeEd448ImportPriv, CompositeEd448ExportPrivOnly, CompositeEd448ExportPub, CompositeEd448Sign, CompositeEd448Verify, @@ -23948,27 +24013,9 @@ int wolfSSH_TestSignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, } else { ret = ops->init(&sigKey.sk.mldsa_composite.base.trad, ssh->ctx->heap); - /* make_key not in CompositeTradOps. */ if (ret == 0) { - if (params.tradType == TRAD_TYPE_ED25519) { -#ifndef WOLFSSH_NO_ED25519 - ret = wc_ed25519_make_key(ssh->rng, ED25519_KEY_SIZE, - &sigKey.sk.mldsa_composite.base.trad.ed25519); -#endif - } - else if (params.tradType == TRAD_TYPE_ED448) { -#ifdef HAVE_ED448 - ret = wc_ed448_make_key(ssh->rng, 57, - &sigKey.sk.mldsa_composite.base.trad.ed448); -#endif - } - else if (params.tradType == TRAD_TYPE_ECC) { -#ifndef WOLFSSH_NO_ECDSA - ret = wc_ecc_make_key_ex(ssh->rng, (int)params.tradPrivSz, - &sigKey.sk.mldsa_composite.base.trad.ecc, - params.eccCurveId); -#endif - } + ret = ops->makeKey(&sigKey.sk.mldsa_composite.base.trad, ssh->rng, + ¶ms); } } diff --git a/src/keygen.c b/src/keygen.c index cb0f8ffb8..58cb3d786 100644 --- a/src/keygen.c +++ b/src/keygen.c @@ -375,95 +375,76 @@ int wolfSSH_MakeMlDsaKey(byte* out, word32 outSz, word32 level) /* Build OpenSSH-key-v1 envelope. */ #if !defined(WOLFSSH_NO_MLDSA) +/* Guard matches MakeCompositeTradKey() callers to avoid unused warnings. */ +#if !defined(WOLFSSH_NO_ED25519) || defined(HAVE_ED448) || \ + !defined(WOLFSSH_NO_ECDSA) +/* Shared init/makeKey/export/free chain for traditional types. */ +static int MakeCompositeTradKeyGeneric(const CompositeTradOps* ops, void* key, + WC_RNG* rng, const CompositeParams* params, byte* tradPub, + byte* tradPriv) +{ + int ret; + word32 sz; + + if (ops->init(key, NULL) != 0) { + return WS_CRYPTO_FAILED; + } + + ret = (ops->makeKey(key, rng, params) == 0) ? 0 : WS_CRYPTO_FAILED; + if (ret == 0) { + sz = params->tradPrivSz; + if (ops->exportPrivOnly(key, tradPriv, &sz) != 0 || + sz != params->tradPrivSz) { + ret = WS_CRYPTO_FAILED; + } + } + if (ret == 0) { + sz = params->tradPubSz; + if (ops->exportPub(key, tradPub, &sz) != 0 || + sz != params->tradPubSz) { + ret = WS_CRYPTO_FAILED; + } + } + ops->free(key); + + return ret; +} +#endif /* traditional algorithm enabled */ + /* Gen trad composite key half. */ static int MakeCompositeTradKey(WC_RNG* rng, const CompositeParams* params, byte* tradPub, byte* tradPriv) { int ret = WS_NOT_COMPILED; + const CompositeTradOps* ops = WS_GetTradOps(params->tradType); WOLFSSH_UNUSED(rng); WOLFSSH_UNUSED(tradPub); WOLFSSH_UNUSED(tradPriv); + if (ops == NULL) { + return WS_NOT_COMPILED; + } + if (params->tradType == TRAD_TYPE_ED25519) { #ifndef WOLFSSH_NO_ED25519 ed25519_key key; - word32 sz; - - if (wc_ed25519_init(&key) != 0) { - return WS_CRYPTO_FAILED; - } - ret = (wc_ed25519_make_key(rng, ED25519_KEY_SIZE, &key) == 0) ? - 0 : WS_CRYPTO_FAILED; - if (ret == 0) { - sz = params->tradPrivSz; - if (wc_ed25519_export_private_only(&key, tradPriv, &sz) != 0 || - sz != params->tradPrivSz) { - ret = WS_CRYPTO_FAILED; - } - } - if (ret == 0) { - sz = params->tradPubSz; - if (wc_ed25519_export_public(&key, tradPub, &sz) != 0 || - sz != params->tradPubSz) { - ret = WS_CRYPTO_FAILED; - } - } - wc_ed25519_free(&key); + ret = MakeCompositeTradKeyGeneric(ops, &key, rng, params, tradPub, + tradPriv); #endif } else if (params->tradType == TRAD_TYPE_ED448) { #ifdef HAVE_ED448 ed448_key key; - word32 sz; - - if (wc_ed448_init(&key) != 0) { - return WS_CRYPTO_FAILED; - } - ret = (wc_ed448_make_key(rng, ED448_KEY_SIZE, &key) == 0) ? - 0 : WS_CRYPTO_FAILED; - if (ret == 0) { - sz = params->tradPrivSz; - if (wc_ed448_export_private_only(&key, tradPriv, &sz) != 0 || - sz != params->tradPrivSz) { - ret = WS_CRYPTO_FAILED; - } - } - if (ret == 0) { - sz = params->tradPubSz; - if (wc_ed448_export_public(&key, tradPub, &sz) != 0 || - sz != params->tradPubSz) { - ret = WS_CRYPTO_FAILED; - } - } - wc_ed448_free(&key); + ret = MakeCompositeTradKeyGeneric(ops, &key, rng, params, tradPub, + tradPriv); #endif } else if (params->tradType == TRAD_TYPE_ECC) { #ifndef WOLFSSH_NO_ECDSA ecc_key key; - word32 sz; - /* Pin ECC curve explicitly. */ - if (wc_ecc_init(&key) != 0) { - return WS_CRYPTO_FAILED; - } - ret = (wc_ecc_make_key_ex(rng, (int)params->tradPrivSz, &key, - params->eccCurveId) == 0) ? 0 : WS_CRYPTO_FAILED; - if (ret == 0) { - sz = params->tradPrivSz; - if (wc_ecc_export_private_only(&key, tradPriv, &sz) != 0 || - sz != params->tradPrivSz) { - ret = WS_CRYPTO_FAILED; - } - } - if (ret == 0) { - sz = params->tradPubSz; - if (wc_ecc_export_x963(&key, tradPub, &sz) != 0 || - sz != params->tradPubSz) { - ret = WS_CRYPTO_FAILED; - } - } - wc_ecc_free(&key); + ret = MakeCompositeTradKeyGeneric(ops, &key, rng, params, tradPub, + tradPriv); #endif } @@ -496,17 +477,17 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, int ret; WC_RNG rng; int rngInit = 0; - MlDsaKey mldsaKey; +#ifdef WOLFSSH_SMALL_STACK + MlDsaKey* mldsaKey = NULL; + byte* mldsaPub = NULL; +#else + MlDsaKey mldsaKeyBuf; + MlDsaKey* mldsaKey = &mldsaKeyBuf; + byte mldsaPub[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ]; +#endif int mldsaInit = 0; int mldsaGenOk; byte mldsaSeed[MLDSA_SEED_SZ]; -#ifndef WOLFSSH_NO_MLDSA87 - byte mldsaPub[WC_MLDSA_87_PUB_KEY_SIZE]; -#elif !defined(WOLFSSH_NO_MLDSA65) - byte mldsaPub[WC_MLDSA_65_PUB_KEY_SIZE]; -#else - byte mldsaPub[WC_MLDSA_44_PUB_KEY_SIZE]; -#endif byte tradPub[COMPOSITE_MAX_TRAD_PUB_SZ]; byte tradPriv[COMPOSITE_MAX_TRAD_PRIV_SZ]; word32 sz; @@ -591,27 +572,37 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, } rngInit = 1; - ret = wc_RNG_GenerateBlock(&rng, mldsaSeed, sizeof(mldsaSeed)); - if (ret != 0) { - ret = WS_CRYPTO_FAILED; + ret = 0; +#ifdef WOLFSSH_SMALL_STACK + mldsaKey = (MlDsaKey*)WMALLOC(sizeof(MlDsaKey), NULL, DYNTYPE_PRIVKEY); + mldsaPub = (byte*)WMALLOC(params.mldsaPubSz, NULL, DYNTYPE_BUFFER); + if (mldsaKey == NULL || mldsaPub == NULL) { + ret = WS_MEMORY_E; } - else { - if (wc_MlDsaKey_Init(&mldsaKey, NULL, INVALID_DEVID) != 0) { +#endif + if (ret == 0) { + ret = wc_RNG_GenerateBlock(&rng, mldsaSeed, sizeof(mldsaSeed)); + if (ret != 0) { ret = WS_CRYPTO_FAILED; } else { - mldsaInit = 1; - if (wc_MlDsaKey_SetParams(&mldsaKey, params.mldsaLevel) != 0 || - wc_MlDsaKey_MakeKeyFromSeed(&mldsaKey, mldsaSeed) != 0) { + if (wc_MlDsaKey_Init(mldsaKey, NULL, INVALID_DEVID) != 0) { ret = WS_CRYPTO_FAILED; } + else { + mldsaInit = 1; + if (wc_MlDsaKey_SetParams(mldsaKey, params.mldsaLevel) != 0 || + wc_MlDsaKey_MakeKeyFromSeed(mldsaKey, mldsaSeed) != 0) { + ret = WS_CRYPTO_FAILED; + } + } } - } - if (ret == 0) { - sz = params.mldsaPubSz; - if (wc_MlDsaKey_ExportPubRaw(&mldsaKey, mldsaPub, &sz) != 0 || - sz != params.mldsaPubSz) { - ret = WS_CRYPTO_FAILED; + if (ret == 0) { + sz = params.mldsaPubSz; + if (wc_MlDsaKey_ExportPubRaw(mldsaKey, mldsaPub, &sz) != 0 || + sz != params.mldsaPubSz) { + ret = WS_CRYPTO_FAILED; + } } } if (ret != 0) { @@ -698,7 +689,7 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, } } - if (mldsaInit) wc_MlDsaKey_Free(&mldsaKey); + if (mldsaInit) wc_MlDsaKey_Free(mldsaKey); if (rngInit) wc_FreeRng(&rng); WS_FORCEZERO(mldsaSeed, sizeof(mldsaSeed)); @@ -709,6 +700,15 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, WFREE(tmpBuf, NULL, DYNTYPE_BUFFER); } +#ifdef WOLFSSH_SMALL_STACK + if (mldsaKey != NULL) { + WFREE(mldsaKey, NULL, DYNTYPE_PRIVKEY); + } + if (mldsaPub != NULL) { + WFREE(mldsaPub, NULL, DYNTYPE_BUFFER); + } +#endif + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_MakeMlDsaCompositeKey(), ret = %d", ret); return ret; diff --git a/src/ossh.c b/src/ossh.c index b09204d6e..e64591ddc 100644 --- a/src/ossh.c +++ b/src/ossh.c @@ -298,7 +298,8 @@ static int GetOpenSshKeyMlDsa(MlDsaKey* key, return ret; } -/* Parse OpenSSH ML-DSA composite private key. */ +/* Parse OpenSSH ML-DSA composite private key. + * Flattens failures to WS_KEY_FORMAT_E except init/memory errors. */ static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, void* tradKey, void* heap, const byte* buf, word32 len, word32* idx) { @@ -342,22 +343,33 @@ static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, ret = wc_MlDsaKey_MakeKeyFromSeed(mldsa, priv); } if (ret == WS_SUCCESS) { -#ifndef WOLFSSH_NO_MLDSA87 - byte mldsaPubCheck[WC_MLDSA_87_PUB_KEY_SIZE]; -#elif !defined(WOLFSSH_NO_MLDSA65) - byte mldsaPubCheck[WC_MLDSA_65_PUB_KEY_SIZE]; +#ifdef WOLFSSH_SMALL_STACK + byte* mldsaPubCheck = NULL; + word32 mldsaPubCheckSz = params.mldsaPubSz; + + mldsaPubCheck = (byte*)WMALLOC(mldsaPubCheckSz, heap, DYNTYPE_BUFFER); + if (mldsaPubCheck == NULL) { + ret = WS_MEMORY_E; + } #else - byte mldsaPubCheck[WC_MLDSA_44_PUB_KEY_SIZE]; -#endif + byte mldsaPubCheck[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ]; word32 mldsaPubCheckSz = sizeof(mldsaPubCheck); +#endif - ret = wc_MlDsaKey_ExportPubRaw(mldsa, mldsaPubCheck, - &mldsaPubCheckSz); - if (ret == 0 && - (mldsaPubCheckSz != params.mldsaPubSz || - WMEMCMP(mldsaPubCheck, pub, params.mldsaPubSz) != 0)) { - ret = WS_KEY_FORMAT_E; + if (ret == WS_SUCCESS) { + ret = wc_MlDsaKey_ExportPubRaw(mldsa, mldsaPubCheck, + &mldsaPubCheckSz); + if (ret == 0 && + (mldsaPubCheckSz != params.mldsaPubSz || + WMEMCMP(mldsaPubCheck, pub, params.mldsaPubSz) != 0)) { + ret = WS_KEY_FORMAT_E; + } + } +#ifdef WOLFSSH_SMALL_STACK + if (mldsaPubCheck) { + WFREE(mldsaPubCheck, heap, DYNTYPE_BUFFER); } +#endif } if (ret == WS_SUCCESS) { ret = ops->importPriv(tradKey, priv + MLDSA_SEED_SZ, params.tradPrivSz, @@ -379,7 +391,9 @@ static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, if (ret != 0) { wc_MlDsaKey_Free(mldsa); ops->free(tradKey); - ret = WS_KEY_FORMAT_E; + if (ret != WS_MEMORY_E) { + ret = WS_KEY_FORMAT_E; + } } return ret; } diff --git a/src/ssh.c b/src/ssh.c index 2e5019f23..42bd5d77d 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2238,14 +2238,7 @@ static int DoAsn1Key(const byte* in, word32 inSz, byte** out, #endif 0) { byte* rawPub = NULL; - word32 rawPubSz = -#ifndef WOLFSSH_NO_MLDSA87 - WC_MLDSA_87_PUB_KEY_SIZE; -#elif !defined(WOLFSSH_NO_MLDSA65) - WC_MLDSA_65_PUB_KEY_SIZE; -#else - WC_MLDSA_44_PUB_KEY_SIZE; -#endif + word32 rawPubSz = WOLFSSH_MLDSA_MAX_PUB_KEY_SZ; const char* name = (const char*)*outType; word32 nameLen = *outTypeSz; word32 localIdx = 0; diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 46325bf57..88cca98f8 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -2347,21 +2347,27 @@ static DWORD SFTP_WinCreationDisp(word32 reason) DWORD disp; if (reason & WOLFSSH_FXF_CREAT) { - if (reason & WOLFSSH_FXF_EXCL) + if (reason & WOLFSSH_FXF_EXCL) { disp = CREATE_NEW; - else if (reason & WOLFSSH_FXF_TRUNC) + } + else if (reason & WOLFSSH_FXF_TRUNC) { disp = CREATE_ALWAYS; - else + } + else { disp = OPEN_ALWAYS; + } } else { /* TRUNCATE_EXISTING requires GENERIC_WRITE in dwDesiredAccess or - * CreateFile() fails with ERROR_INVALID_PARAMETER; without WRITE - * there is no way to truncate, so fall back to OPEN_EXISTING. */ - if ((reason & WOLFSSH_FXF_TRUNC) && (reason & WOLFSSH_FXF_WRITE)) + * CreateFile() fails with ERROR_INVALID_PARAMETER. TRUNC without + * WRITE is deliberately ignored and the open succeeds untruncated, + * as O_RDONLY|O_TRUNC does on most POSIX systems. */ + if ((reason & WOLFSSH_FXF_TRUNC) && (reason & WOLFSSH_FXF_WRITE)) { disp = TRUNCATE_EXISTING; - else + } + else { disp = OPEN_EXISTING; + } } return disp; @@ -2692,12 +2698,15 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) } #endif - if (reason & WOLFSSH_FXF_READ) + if (reason & WOLFSSH_FXF_READ) { desiredAccess |= GENERIC_READ; - if (reason & WOLFSSH_FXF_WRITE) + } + if (reason & WOLFSSH_FXF_WRITE) { desiredAccess |= GENERIC_WRITE; - if (reason & WOLFSSH_FXF_APPEND) + } + if (reason & WOLFSSH_FXF_APPEND) { desiredAccess |= FILE_APPEND_DATA; + } creationDisp = SFTP_WinCreationDisp(reason); @@ -4265,6 +4274,7 @@ int wolfSSH_SFTP_RecvWrite(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) WFD fd = 0; int ret = WS_SUCCESS; int rc; + int isAppend = 0; word32 idx = 0; word32 ofst[2] = {0,0}; @@ -4279,6 +4289,7 @@ int wolfSSH_SFTP_RecvWrite(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) if (ssh == NULL) { return WS_BAD_ARGUMENT; } + WOLFSSH_UNUSED(isAppend); /* only read on ports that define WWRITE */ WLOG(WS_LOG_SFTP, "Receiving WOLFSSH_FTP_WRITE"); @@ -4313,6 +4324,7 @@ int wolfSSH_SFTP_RecvWrite(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) } else { fd = fileEntry->fd; + isAppend = fileEntry->isAppend; } } } @@ -4335,8 +4347,20 @@ int wolfSSH_SFTP_RecvWrite(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) /* Retry while WPWRITE makes forward progress; bail on error * or zero return to avoid spinning on a stuck backend. */ while (written < strSz) { - ret = WPWRITE(ssh->fs, fd, (byte*)str + written, - strSz - written, ofst); + #ifdef WWRITE + if (isAppend) { + /* FXF_APPEND: the offset is ignored and the O_APPEND + * fd puts the write at EOF. pwrite() would honor the + * offset on every POSIX system but Linux. */ + ret = WWRITE(ssh->fs, fd, (byte*)str + written, + strSz - written); + } + else + #endif + { + ret = WPWRITE(ssh->fs, fd, (byte*)str + written, + strSz - written, ofst); + } if (ret <= 0) { break; } @@ -4459,8 +4483,8 @@ int wolfSSH_SFTP_RecvWrite(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) * non-atomic read-modify-write: the file is shared FILE_SHARE_WRITE, * so concurrent appenders would resolve the same offset and overwrite * each other. Setting both OVERLAPPED offset fields to 0xFFFFFFFF - * tells WriteFile() to append atomically at end of file, matching the - * POSIX O_APPEND path. */ + * tells WriteFile() to append atomically at end of file, matching + * the POSIX O_APPEND path. */ if (isAppend) { offset.Offset = 0xFFFFFFFF; offset.OffsetHigh = 0xFFFFFFFF; @@ -4473,7 +4497,9 @@ int wolfSSH_SFTP_RecvWrite(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) return WS_BUFFER_E; } - if (WriteFile(fd, str, strSz, &bytesWritten, &offset) == 0) { + /* A short write is a failure too, as on the POSIX side. */ + if (WriteFile(fd, str, strSz, &bytesWritten, &offset) == 0 || + bytesWritten != strSz) { WLOG(WS_LOG_SFTP, "Error writing to file"); res = err; type = WOLFSSH_FTP_FAILURE; diff --git a/tests/kex.c b/tests/kex.c index be3cea65d..08c00064c 100644 --- a/tests/kex.c +++ b/tests/kex.c @@ -518,6 +518,7 @@ int wolfSSH_KexTest(int argc, char** argv) #ifndef WOLFSSH_NO_MLDSA87 AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa-87"), EXIT_SUCCESS); #endif +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ !defined(NO_SHA512) /* Uses the "@openssh.com" wire name that OpenSSH negotiates for this @@ -549,6 +550,7 @@ int wolfSSH_KexTest(int argc, char** argv) AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa87-ed448@wolfssl.com"), EXIT_SUCCESS); #endif +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ AssertIntEQ(wolfSSH_Cleanup(), WS_SUCCESS); diff --git a/tests/regress.c b/tests/regress.c index 06467974a..c7a3b250d 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -7659,6 +7659,146 @@ static void AssertSftpStatusReply(WOLFSSH* ssh, int reqId, word32 code) AssertIntEQ((int)SftpGetU32(reply + LENGTH_SZ + MSG_ID_SZ), reqId); AssertIntEQ((int)SftpGetU32(reply + WOLFSSH_SFTP_HEADER), (int)code); } + +/* The FXP_HANDLE reply carries the handle after the header and its length. */ +#define SFTP_TEST_HANDLE_OFF (WOLFSSH_SFTP_HEADER + UINT32_SZ) + +#ifdef USE_WINDOWS_API + #define SFTP_TEST_PID() ((unsigned long)GetCurrentProcessId()) +#else + #define SFTP_TEST_PID() ((unsigned long)getpid()) +#endif + +/* FXP_OPEN path with flags and empty attributes. On success the handle from + * the FXP_HANDLE reply is copied to handle. */ +static int SftpOpenPath(WOLFSSH* ssh, int reqId, const char* path, + word32 flags, byte* handle) +{ + byte pkt[WOLFSSH_MAX_FILENAME + 3 * UINT32_SZ]; + word32 idx = 0; + word32 pathSz = (word32)WSTRLEN(path); + int ret; + + SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; + SftpPutU32(flags, pkt + idx); idx += UINT32_SZ; + SftpPutU32(0, pkt + idx); idx += UINT32_SZ; + + ret = wolfSSH_SFTP_RecvOpen(ssh, reqId, pkt, idx); + if (ret == WS_SUCCESS) { + const byte* reply; + word32 replySz; + + reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); + AssertNotNull(reply); + AssertTrue(replySz >= SFTP_TEST_HANDLE_OFF + WOLFSSH_HANDLE_ID_SZ); + /* no request id check: RecvOpen stamps the reply with ssh->reqId, + * which the dispatch loop sets and a direct call leaves at 0 */ + AssertIntEQ(reply[LENGTH_SZ], WOLFSSH_FTP_HANDLE); + WMEMCPY(handle, reply + SFTP_TEST_HANDLE_OFF, WOLFSSH_HANDLE_ID_SZ); + } + + return ret; +} + +/* FXP_WRITE dataSz bytes at the split 64-bit offset through handle. */ +static int SftpWriteHandle(WOLFSSH* ssh, int reqId, const byte* handle, + word32 ofstHi, word32 ofstLo, const void* data, word32 dataSz) +{ + byte pkt[64 + WOLFSSH_HANDLE_ID_SZ + 4 * UINT32_SZ]; + word32 idx = 0; + + AssertTrue(dataSz <= 64); + SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); + idx += WOLFSSH_HANDLE_ID_SZ; + SftpPutU32(ofstHi, pkt + idx); idx += UINT32_SZ; + SftpPutU32(ofstLo, pkt + idx); idx += UINT32_SZ; + SftpPutU32(dataSz, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, data, dataSz); idx += dataSz; + + return wolfSSH_SFTP_RecvWrite(ssh, reqId, pkt, idx); +} + +/* FXP_CLOSE handle. */ +static int SftpCloseHandle(WOLFSSH* ssh, int reqId, const byte* handle) +{ + byte pkt[WOLFSSH_HANDLE_ID_SZ + UINT32_SZ]; + word32 idx = 0; + + SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); + idx += WOLFSSH_HANDLE_ID_SZ; + + return wolfSSH_SFTP_RecvClose(ssh, reqId, pkt, idx); +} + +/* Assert the file at path holds exactly expSz bytes matching exp. */ +static void AssertSftpFileContents(const char* path, const void* exp, + word32 expSz) +{ + WFILE* file; + char readBuf[64]; + word32 readSz; + + AssertTrue(expSz < sizeof(readBuf)); + AssertIntEQ(WFOPEN(NULL, &file, path, "rb"), 0); + AssertTrue(file != WBADFILE); + readSz = (word32)WFREAD(NULL, readBuf, 1, sizeof(readBuf), file); + WFCLOSE(NULL, file); + AssertIntEQ((int)readSz, (int)expSz); + AssertIntEQ(WMEMCMP(readBuf, exp, expSz), 0); +} + +/* FXF_APPEND puts every write at EOF whatever offset the client sends + * (draft-ietf-secsh-filexfer-02 6.3). On Windows that is the OVERLAPPED + * 0xFFFFFFFF encoding; on POSIX it needs write() rather than pwrite(), + * which ignores O_APPEND everywhere but Linux. */ +static void TestSftpAppendWritesAtEof(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + int rid = 600; + WSTAT_T st; + byte handle[WOLFSSH_HANDLE_ID_SZ]; + char cwd[WOLFSSH_MAX_FILENAME]; + char path[64]; + const char content[] = "0123456789"; + const char content2[] = "abcde"; + const char expect[] = "0123456789abcde"; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(wolfSSH_SFTP_TestRecvStateInit(ssh), WS_SUCCESS); + + WSNPRINTF(path, sizeof(path), "wolfssh_append_%lu.tmp", SFTP_TEST_PID()); + WMEMSET(cwd, 0, sizeof(cwd)); + AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1)); + AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS); + (void)WREMOVE(ssh->fs, path); + + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_APPEND | WOLFSSH_FXF_CREAT, + handle), WS_SUCCESS); + /* first write at offset 0 lands at EOF 0 */ + AssertIntEQ(SftpWriteHandle(ssh, rid++, handle, 0, 0, + content, (word32)(sizeof(content) - 1)), WS_SUCCESS); + /* stale offset 0 again: must append, not overwrite the start */ + AssertIntEQ(SftpWriteHandle(ssh, rid++, handle, 0, 0, + content2, (word32)(sizeof(content2) - 1)), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); + + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_size, (int)(sizeof(expect) - 1)); + AssertSftpFileContents(path, expect, (word32)(sizeof(expect) - 1)); + + (void)WREMOVE(ssh->fs, path); + wolfSSH_SFTP_TestRecvStateFree(ssh); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} #endif /* !NO_WOLFSSH_SERVER && !NO_FILESYSTEM */ #if !defined(NO_WOLFSSH_SERVER) && !defined(USE_WINDOWS_API) && \ @@ -8748,21 +8888,14 @@ static void TestSftpWindowsOpenFlagMatrix(void) WOLFSSH* ssh; int rid = 500; int reqId; - word32 idx; - word32 replySz; - const byte* reply; - const word32 hOff = WOLFSSH_SFTP_HEADER + UINT32_SZ; /* handle in reply */ WSTAT_T st; byte handle[WOLFSSH_HANDLE_ID_SZ]; - byte pkt[256]; char cwd[WOLFSSH_MAX_FILENAME]; char path[64]; - word32 pathSz; const char content[] = "0123456789"; const char content2[] = "abcde"; - WFILE* file; - char readBuf[32]; - word32 readSz; + const word32 contentSz = (word32)(sizeof(content) - 1); + const word32 content2Sz = (word32)(sizeof(content2) - 1); ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); AssertNotNull(ctx); @@ -8772,8 +8905,7 @@ static void TestSftpWindowsOpenFlagMatrix(void) /* unique per-process fixture name so parallel runs don't collide */ WSNPRINTF(path, sizeof(path), "wolfssh_winflags_%lu.tmp", - (unsigned long)GetCurrentProcessId()); - pathSz = (word32)WSTRLEN(path); + SFTP_TEST_PID()); WMEMSET(cwd, 0, sizeof(cwd)); AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1)); @@ -8782,390 +8914,123 @@ static void TestSftpWindowsOpenFlagMatrix(void) (void)WREMOVE(ssh->fs, path); /* WRITE only, no CREAT: must fail against a missing file, untouched. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE, pkt + idx); idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; reqId = rid++; - AssertTrue(wolfSSH_SFTP_RecvOpen(ssh, reqId, pkt, idx) != WS_SUCCESS); + AssertTrue(SftpOpenPath(ssh, reqId, path, WOLFSSH_FXF_WRITE, handle) + != WS_SUCCESS); AssertSftpStatusReply(ssh, reqId, WOLFSSH_FTP_FAILURE); AssertTrue(WSTAT(ssh->fs, path, &st) != 0); - /* WRITE|CREAT, no TRUNC: must create the missing file (OPEN_ALWAYS). */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, pkt + idx); - idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - - /* seed content through the handle just opened */ - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset hi */ - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset lo */ - SftpPutU32((word32)(sizeof(content) - 1), pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, content, sizeof(content) - 1); - idx += (word32)(sizeof(content) - 1); - AssertIntEQ(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx), WS_SUCCESS); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); - + /* WRITE|CREAT, no TRUNC: must create the missing file (OPEN_ALWAYS). + * Seed content through the handle just opened. */ + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, handle), WS_SUCCESS); + AssertIntEQ(SftpWriteHandle(ssh, rid++, handle, 0, 0, content, + contentSz), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); - AssertIntEQ((int)st.st_size, (int)(sizeof(content) - 1)); + AssertIntEQ((int)st.st_size, (int)contentSz); /* WRITE|CREAT, no TRUNC, on the existing file: must not truncate it. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, pkt + idx); - idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); - AssertIntEQ((int)st.st_size, (int)(sizeof(content) - 1)); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + AssertIntEQ((int)st.st_size, (int)contentSz); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); /* WRITE|CREAT|TRUNC: must truncate the existing file immediately. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC, - pkt + idx); idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC, + handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); AssertIntEQ((int)st.st_size, 0); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); /* WRITE|CREAT|EXCL against the existing file: must fail. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_EXCL, - pkt + idx); idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; reqId = rid++; - AssertTrue(wolfSSH_SFTP_RecvOpen(ssh, reqId, pkt, idx) != WS_SUCCESS); + AssertTrue(SftpOpenPath(ssh, reqId, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_EXCL, + handle) != WS_SUCCESS); AssertSftpStatusReply(ssh, reqId, WOLFSSH_FTP_FAILURE); (void)WREMOVE(ssh->fs, path); /* WRITE|CREAT|EXCL against a missing path: must succeed. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_EXCL, - pkt + idx); idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_EXCL, + handle), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); (void)WREMOVE(ssh->fs, path); /* READ|WRITE|CREAT, no TRUNC, against a missing path: must create it. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, - pkt + idx); idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, + handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); (void)WREMOVE(ssh->fs, path); /* WRITE|TRUNC, no CREAT, against a missing path: must fail * (TRUNCATE_EXISTING requires the file to already exist). */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_TRUNC, pkt + idx); - idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; reqId = rid++; - AssertTrue(wolfSSH_SFTP_RecvOpen(ssh, reqId, pkt, idx) != WS_SUCCESS); + AssertTrue(SftpOpenPath(ssh, reqId, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_TRUNC, handle) != WS_SUCCESS); AssertSftpStatusReply(ssh, reqId, WOLFSSH_FTP_FAILURE); /* seed an existing file with content, then WRITE|TRUNC, no CREAT: must * truncate it immediately (TRUNCATE_EXISTING). */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, pkt + idx); - idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset hi */ - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset lo */ - SftpPutU32((word32)(sizeof(content) - 1), pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, content, sizeof(content) - 1); - idx += (word32)(sizeof(content) - 1); - AssertIntEQ(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx), WS_SUCCESS); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); - + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, handle), WS_SUCCESS); + AssertIntEQ(SftpWriteHandle(ssh, rid++, handle, 0, 0, content, + contentSz), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); - AssertIntEQ((int)st.st_size, (int)(sizeof(content) - 1)); - - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_TRUNC, pkt + idx); - idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); + AssertIntEQ((int)st.st_size, (int)contentSz); + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_TRUNC, handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); AssertIntEQ((int)st.st_size, 0); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); (void)WREMOVE(ssh->fs, path); /* seed an existing file with content for the READ|TRUNC case below. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, pkt + idx); - idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset hi */ - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset lo */ - SftpPutU32((word32)(sizeof(content) - 1), pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, content, sizeof(content) - 1); - idx += (word32)(sizeof(content) - 1); - AssertIntEQ(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx), WS_SUCCESS); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); - + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, handle), WS_SUCCESS); + AssertIntEQ(SftpWriteHandle(ssh, rid++, handle, 0, 0, content, + contentSz), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); - AssertIntEQ((int)st.st_size, (int)(sizeof(content) - 1)); + AssertIntEQ((int)st.st_size, (int)contentSz); /* READ|TRUNC, no WRITE, no CREAT: TRUNCATE_EXISTING requires * GENERIC_WRITE in dwDesiredAccess, so this must still open (falling * back to OPEN_EXISTING) rather than fail with ERROR_INVALID_PARAMETER, * and must leave the content untouched since it cannot actually * truncate. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_TRUNC, pkt + idx); - idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - reqId = rid++; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, reqId, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - - AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); - AssertIntEQ((int)st.st_size, (int)(sizeof(content) - 1)); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); - - (void)WREMOVE(ssh->fs, path); - - /* WRITE|APPEND|CREAT against a missing path: must create it, and every - * write must land at EOF regardless of the client-supplied offset. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_APPEND | WOLFSSH_FXF_CREAT, - pkt + idx); idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - - /* first write, offset 0: lands at EOF (0), file becomes "0123456789" */ - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset hi */ - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset lo */ - SftpPutU32((word32)(sizeof(content) - 1), pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, content, sizeof(content) - 1); - idx += (word32)(sizeof(content) - 1); - AssertIntEQ(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx), WS_SUCCESS); - - /* second write, offset stale at 0 again: must still append at EOF (10) - * rather than overwrite the start of the file. */ - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset hi */ - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset lo */ - SftpPutU32((word32)(sizeof(content2) - 1), pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, content2, sizeof(content2) - 1); - idx += (word32)(sizeof(content2) - 1); - AssertIntEQ(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx), WS_SUCCESS); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); - + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_READ | WOLFSSH_FXF_TRUNC, handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); - AssertIntEQ((int)st.st_size, - (int)(sizeof(content) - 1 + sizeof(content2) - 1)); - - /* content, not just length: a build that appended the right byte - * count in the wrong order, or left a gap, would still pass the size - * check above. */ - AssertIntEQ(WFOPEN(NULL, &file, path, "rb"), 0); - AssertTrue(file != WBADFILE); - readSz = (word32)WFREAD(NULL, readBuf, 1, - sizeof(content) - 1 + sizeof(content2) - 1, file); - WFCLOSE(NULL, file); - AssertIntEQ((int)readSz, - (int)(sizeof(content) - 1 + sizeof(content2) - 1)); - AssertIntEQ(WMEMCMP(readBuf, "0123456789abcde", readSz), 0); + AssertIntEQ((int)st.st_size, (int)contentSz); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); (void)WREMOVE(ssh->fs, path); /* APPEND|CREAT, no WRITE: FILE_APPEND_DATA alone is enough to create * the file and append to it, unlike master, which could not open this - * combination at all. */ - idx = 0; - SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, path, pathSz); idx += pathSz; - SftpPutU32(WOLFSSH_FXF_APPEND | WOLFSSH_FXF_CREAT, pkt + idx); - idx += UINT32_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); - reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); - AssertNotNull(reply); - AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); - WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset hi */ - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset lo */ - SftpPutU32((word32)(sizeof(content) - 1), pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, content, sizeof(content) - 1); - idx += (word32)(sizeof(content) - 1); - AssertIntEQ(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx), WS_SUCCESS); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset hi */ - SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset lo */ - SftpPutU32((word32)(sizeof(content2) - 1), pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, content2, sizeof(content2) - 1); - idx += (word32)(sizeof(content2) - 1); - AssertIntEQ(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx), WS_SUCCESS); - - idx = 0; - SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; - WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); - idx += WOLFSSH_HANDLE_ID_SZ; - AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); - + * combination at all. (WRITE|APPEND|CREAT is covered on every platform + * by TestSftpAppendWritesAtEof.) */ + AssertIntEQ(SftpOpenPath(ssh, rid++, path, + WOLFSSH_FXF_APPEND | WOLFSSH_FXF_CREAT, handle), WS_SUCCESS); + AssertIntEQ(SftpWriteHandle(ssh, rid++, handle, 0, 0, content, + contentSz), WS_SUCCESS); + AssertIntEQ(SftpWriteHandle(ssh, rid++, handle, 0, 0, content2, + content2Sz), WS_SUCCESS); + AssertIntEQ(SftpCloseHandle(ssh, rid++, handle), WS_SUCCESS); AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); - AssertIntEQ((int)st.st_size, - (int)(sizeof(content) - 1 + sizeof(content2) - 1)); + AssertIntEQ((int)st.st_size, (int)(contentSz + content2Sz)); (void)WREMOVE(ssh->fs, path); wolfSSH_SFTP_TestRecvStateFree(ssh); @@ -12018,6 +11883,10 @@ int main(int argc, char** argv) /* SETSTAT/FSETSTAT apply the attributes they acknowledge */ TestSftpSetStatAttributes(); #endif + #if !defined(NO_WOLFSSH_SERVER) && !defined(NO_FILESYSTEM) + /* FXF_APPEND writes land at EOF regardless of the request offset */ + TestSftpAppendWritesAtEof(); + #endif #if !defined(NO_WOLFSSH_SERVER) && defined(USE_WINDOWS_API) && \ !defined(NO_FILESYSTEM) /* RecvOpen's Windows open-flag matrix */ diff --git a/tests/unit.c b/tests/unit.c index 22c07a602..0f58c4f26 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -994,6 +994,7 @@ static int test_MlDsaKeyGen(void) } /* Round-trip test for ML-DSA composite keys. */ +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES static int test_MlDsaCompositeKeyGen(void) { /* NULL-terminated so the table is never empty if ECDSA and @@ -1144,6 +1145,7 @@ static int test_MlDsaCompositeKeyGen(void) return result; } +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ #endif /* WOLFSSH_NO_MLDSA */ @@ -13008,6 +13010,7 @@ static int test_DoUserAuthRequestMlDsa(void) * 3 = claim a composite signature length shorter than the ML-DSA half * alone, 4 = claim a composite signature length far longer than what was * produced. All tamper modes must be rejected by verification. */ +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, byte keyId, int tamperSig) { @@ -13749,9 +13752,11 @@ static int test_PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope(void) return 0; #endif } +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ /* Test client signer against server parser. */ #ifdef WOLFSSH_KEYGEN +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES static int test_BuildUserAuthRequestMlDsaComposite_Params( const char* keyTypeName, word32 level, word32 tradType, byte keyId) { @@ -14008,8 +14013,10 @@ static int test_BuildUserAuthRequestMlDsaComposite(void) #endif return 0; } +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ #endif /* WOLFSSH_KEYGEN */ +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES static int test_DoUserAuthRequestMlDsaComposite(void) { int ret = 0; @@ -14121,6 +14128,7 @@ static int test_DoUserAuthRequestMlDsaComposite(void) #endif return 0; } +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ #ifdef WOLFSSH_KEYGEN static int test_PrepareUserAuthRequestMlDsa(void) @@ -20287,6 +20295,7 @@ int wolfSSH_UnitTest(int argc, char** argv) printf("DoUserAuthRequestMlDsa: %s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); testResult = testResult || (unitResult != 0); +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES unitResult = test_DoUserAuthRequestMlDsaComposite(); printf("DoUserAuthRequestMlDsaComposite: %s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); @@ -20300,11 +20309,14 @@ int wolfSSH_UnitTest(int argc, char** argv) "%s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); testResult = testResult || (unitResult != 0); +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ #ifdef WOLFSSH_KEYGEN +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES unitResult = test_BuildUserAuthRequestMlDsaComposite(); printf("BuildUserAuthRequestMlDsaComposite: %s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); testResult = testResult || (unitResult != 0); +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ unitResult = test_PrepareUserAuthRequestMlDsa(); printf("PrepareUserAuthRequestMlDsa: %s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); @@ -20588,10 +20600,12 @@ int wolfSSH_UnitTest(int argc, char** argv) unitResult = test_MlDsaKeyGen(); printf("MlDsaKeyGen: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; +#ifndef WOLFSSH_NO_MLDSA_COMPOSITES unitResult = test_MlDsaCompositeKeyGen(); printf("MlDsaCompositeKeyGen: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; +#endif /* WOLFSSH_NO_MLDSA_COMPOSITES */ #endif #endif /* WOLFSSH_KEYGEN */ unitResult = test_OpenSshPemNegative(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 2201d4caa..833a2eb77 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -370,6 +370,14 @@ extern "C" { #undef WOLFSSH_NO_PUBKEY_AUTH #define WOLFSSH_NO_PUBKEY_AUTH #endif +/* A composite needs an ML-DSA level and a traditional algorithm. */ +#if (defined(WOLFSSH_NO_MLDSA44) && defined(WOLFSSH_NO_MLDSA65) && \ + defined(WOLFSSH_NO_MLDSA87)) || \ + (defined(WOLFSSH_NO_ECDSA) && defined(WOLFSSH_NO_ED25519) && \ + !defined(HAVE_ED448)) + #undef WOLFSSH_NO_MLDSA_COMPOSITES + #define WOLFSSH_NO_MLDSA_COMPOSITES +#endif #if defined(WOLFSSH_NO_RSA) || \ (defined(WOLFSSH_NO_RSA_SHA2_256) && defined(WOLFSSH_NO_RSA_SHA2_512)) #undef WOLFSSH_NO_OSSH_CERT_RSA @@ -1495,6 +1503,14 @@ typedef struct WS_KeySignature { #define COMPOSITE_MAX_LABEL_SZ 33 #define ECC_P256_COORD_SZ 32 #define ECC_P384_COORD_SZ 48 +/* Max enabled ML-DSA raw public key size for buffers. */ +#ifndef WOLFSSH_NO_MLDSA87 +#define WOLFSSH_MLDSA_MAX_PUB_KEY_SZ WC_MLDSA_87_PUB_KEY_SIZE +#elif !defined(WOLFSSH_NO_MLDSA65) +#define WOLFSSH_MLDSA_MAX_PUB_KEY_SZ WC_MLDSA_65_PUB_KEY_SIZE +#else +#define WOLFSSH_MLDSA_MAX_PUB_KEY_SZ WC_MLDSA_44_PUB_KEY_SIZE +#endif /* max trad pubkey size */ #define COMPOSITE_MAX_TRAD_PUB_SZ (1 + (2 * ECC_P384_COORD_SZ)) /* max trad privkey size */ @@ -1532,6 +1548,7 @@ typedef struct CompositeParams { typedef struct CompositeTradOps { int (*init)(void* key, void* heap); void (*free)(void* key); + int (*makeKey)(void* key, WC_RNG* rng, const CompositeParams* params); int (*importPub)(void* key, const byte* pub, word32 pubSz); int (*importPriv)(void* key, const byte* priv, word32 privSz, const byte* pub, word32 pubSz); diff --git a/wolfssh/port.h b/wolfssh/port.h index 972acc76a..24abb2fba 100644 --- a/wolfssh/port.h +++ b/wolfssh/port.h @@ -1630,6 +1630,10 @@ extern "C" { const unsigned int* shortOffset); #define WPWRITE(fs,fd,b,s,o) wPwrite((fd),(b),(s),(o)) #define WPREAD(fs,fd,b,s,o) wPread((fd),(b),(s),(o)) + /* Positionless write, for O_APPEND handles: pwrite() only honors + * O_APPEND on Linux, write() does everywhere. */ + #define WWRITE(fs,fd,b,s) (int)write((fd),(b),(s)) + #define WTRUNCATE(fs,f,sz) truncate((f),(off_t)(sz)) #define WFTRUNCATE(fs,fd,sz) ftruncate((fd),(off_t)(sz))