diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 4ed6bd81a..44605bcd1 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -53,6 +53,7 @@ #include #include #include +#include #ifdef WOLFSSL_FPKI #include @@ -147,20 +148,56 @@ struct WOLFSSHD_AUTH { #endif #ifndef MAX_LINE_SZ - /* Sized to hold the largest authorized_keys entry. */ + /* sized for the largest authorized_keys entry, composite pubkeys + * included */ #ifndef WOLFSSH_NO_MLDSA #ifndef WOLFSSH_NO_MLDSA87 - #define MAX_LINE_SZ ((WC_MLDSA_87_PUB_KEY_SIZE + 2) / 3 * 4 + 640) + #if defined(WOLFSSH_CERTS) + /* x509v3-ssh-mldsa-87 size (pubkey+CA sig+DER, base64); + * COMPOSITE_MAX_TRAD_PUB_SZ is headroom for a future variant. */ + #define MAX_LINE_SZ \ + ((WC_MLDSA_87_PUB_KEY_SIZE + WC_MLDSA_87_SIG_SIZE + \ + COMPOSITE_MAX_TRAD_PUB_SZ + 1024 + 2) / 3 * 4 + 640) + #else + #define MAX_LINE_SZ \ + ((WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ + \ + 2) / 3 * 4 + 640) + #endif #elif !defined(WOLFSSH_NO_MLDSA65) - #define MAX_LINE_SZ ((WC_MLDSA_65_PUB_KEY_SIZE + 2) / 3 * 4 + 640) + #if defined(WOLFSSH_CERTS) + #define MAX_LINE_SZ \ + ((WC_MLDSA_65_PUB_KEY_SIZE + WC_MLDSA_65_SIG_SIZE + \ + COMPOSITE_MAX_TRAD_PUB_SZ + 1024 + 2) / 3 * 4 + 640) + #else + #define MAX_LINE_SZ \ + ((WC_MLDSA_65_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ + \ + 2) / 3 * 4 + 640) + #endif #else - #define MAX_LINE_SZ ((WC_MLDSA_44_PUB_KEY_SIZE + 2) / 3 * 4 + 640) + #if defined(WOLFSSH_CERTS) + #define MAX_LINE_SZ \ + ((WC_MLDSA_44_PUB_KEY_SIZE + WC_MLDSA_44_SIG_SIZE + \ + COMPOSITE_MAX_TRAD_PUB_SZ + 1024 + 2) / 3 * 4 + 640) + #else + #define MAX_LINE_SZ \ + ((WC_MLDSA_44_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ + \ + 2) / 3 * 4 + 640) + #endif #endif #else #define MAX_LINE_SZ 900 #endif #endif +#ifdef WOLFSSHD_UNIT_TEST +/* Exposes MAX_LINE_SZ so tests can size worst-case lines without + * duplicating the formula above. */ +word32 wolfsshd_test_MaxLineSz(void) +{ + return (word32)MAX_LINE_SZ; +} +#endif + #if 0 /* this could potentially be useful in a deeply embedded future port */ @@ -257,6 +294,28 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, #endif #endif #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + "ssh-mldsa44-es256@wolfssl.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256@wolfssl.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384@wolfssl.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa44-ed25519@openssh.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa65-ed25519@wolfssl.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448@wolfssl.com", + #endif }; const int NUM_ALLOWED_TYPES = (int)(sizeof(allowedTypes) / sizeof(allowedTypes[0])); @@ -996,6 +1055,84 @@ static int SearchKeysFile(const char* keysFilePath, const byte* key, return ret; } +/* Detects OpenSSH vs ASN1/DER format of a raw host private key buffer. + * + * Uses wc_KeyPemToDer(), not wc_PemToDer(..., PRIVATEKEY_TYPE, ...): the + * latter also unwraps PKCS#8 via ToTraditional(), which mangles key types + * with no traditional DER form (e.g. ML-DSA). + * + * On a PEM buffer, *keyDer is a WMALLOC'd (heap, DYNTYPE_SSHD) buffer the + * caller must WS_FORCEZERO + WFREE; NULL if data was passed through as-is + * (raw DER or OpenSSH). privBuf/privBufSz are set to the buffer to actually + * load. Returns WOLFSSH_FORMAT_ASN1/WOLFSSH_FORMAT_OPENSSH, or negative on + * error. */ +int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, void* heap, + byte** keyDer, byte** privBuf, word32* privBufSz) +{ + int keyFormat = WOLFSSH_FORMAT_ASN1; + byte* der; + int derSz; + + if (keyDer != NULL) { + *keyDer = NULL; + } + if (privBuf != NULL) { + *privBuf = NULL; + } + if (privBufSz != NULL) { + *privBufSz = 0; + } + + if (data == NULL || dataSz == 0 || keyDer == NULL || privBuf == NULL || + privBufSz == NULL) { + return WS_BAD_ARGUMENT; + } + + der = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD); + if (der == NULL) { + return WS_MEMORY_E; + } + + derSz = wc_KeyPemToDer(data, (int)dataSz, der, (int)dataSz, NULL); + if (derSz <= 0) { + WS_FORCEZERO(der, dataSz); + WFREE(der, heap, DYNTYPE_SSHD); + + *privBuf = data; + *privBufSz = dataSz; + + /* Strict prefix match for OpenSSH magic (35 bytes) */ + if (*privBufSz >= 35 && + WMEMCMP(*privBuf, "-----BEGIN OPENSSH PRIVATE KEY-----", 35) == 0) { + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + else if (*privBufSz >= sizeof("openssh-key-v1") && + WMEMCMP(*privBuf, "openssh-key-v1", + sizeof("openssh-key-v1")) == 0) { + /* sizeof() includes the magic's trailing NUL */ + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + else if (data[0] != 0x30) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to convert host private key from PEM."); + *privBuf = NULL; + *privBufSz = 0; + return WS_BAD_FILE_E; + } + } + else { + *keyDer = der; + *privBuf = der; + *privBufSz = (word32)derSz; + /* PEM-decoded result may still be an OpenSSH binary blob */ + if (*privBufSz >= sizeof("openssh-key-v1") && + WMEMCMP(*privBuf, "openssh-key-v1", + sizeof("openssh-key-v1")) == 0) { + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + } + + return keyFormat; +} WOLFSSHD_STATIC int SearchForPubKey(const char* path, const char* authKeysFile, const char* user, diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 78bc2c905..0de35dff6 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -101,6 +101,12 @@ int wolfSSHD_GetHomeDirectory(WOLFSSHD_AUTH* auth, WOLFSSH* ssh, WCHAR* out, int int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid, int rejectReadable, void* heap, WFILE** out); +/* classifies a loaded host private key buffer as OpenSSH or ASN1/DER. + * *keyDer is a WMALLOC'd (heap, DYNTYPE_SSHD) buffer to WS_FORCEZERO + + * WFREE on a PEM decode, else NULL. */ +int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, void* heap, + byte** keyDer, byte** privBuf, word32* privBufSz); + #ifdef WOLFSSHD_UNIT_TEST #ifndef _WIN32 extern int (*wsshd_setregid_cb)(WGID_T, WGID_T); @@ -118,6 +124,7 @@ int SearchForPubKey(const char* path, const char* authKeysFile, const WS_UserAuthData_PublicKey* pubKeyCtx, WUID_T uid, int strictModes); #endif +word32 wolfsshd_test_MaxLineSz(void); #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) int CheckPasswordHashUnix(const char* input, char* stored); #endif diff --git a/apps/wolfsshd/test/create_sshd_config.sh b/apps/wolfsshd/test/create_sshd_config.sh index 574bdedea..035e5c920 100755 --- a/apps/wolfsshd/test/create_sshd_config.sh +++ b/apps/wolfsshd/test/create_sshd_config.sh @@ -16,6 +16,20 @@ AuthorizedKeysFile $PWD/authorized_keys_test EOF +cat < sshd_config_test_mldsa +Port 22222 +Protocol 2 +LoginGraceTime 600 +PermitRootLogin yes +PasswordAuthentication yes +PermitEmptyPasswords no +UsePrivilegeSeparation no +UseDNS no +HostKey $PWD/../../../keys/server-key-mldsa87es384 +AuthorizedKeysFile $PWD/authorized_keys_test + +EOF + cat < sshd_config_test_x509 Port 22222 Protocol 2 diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index e38831029..7c8ed4d8c 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -11,6 +11,7 @@ test_cases=( "sshd_scp_fail.sh" "sshd_term_close_test.sh" "ssh_kex_algos.sh" + "sshd_mldsa_composite_test.sh" ) # Set defaults @@ -424,6 +425,15 @@ else stop_wolfsshd fi + # ML-DSA composite host key test. Runs when we control the local daemon. + # The client side uses an ECC key since we only test the host key here. + if [ "$USING_LOCAL_HOST" == 1 ]; then + start_wolfsshd "sshd_config_test_mldsa" + run_test "sshd_mldsa_composite_test.sh" + printf "Shutting down test wolfSSHd\n" + stop_wolfsshd + fi + # OpenSSH certificate user-auth test (self-contained: starts its own # wolfSSHd; skips when not built with --enable-ossh-certs). Runs the suite # against the wolfSSH example client and, for interop, the system OpenSSH diff --git a/apps/wolfsshd/test/sshd_mldsa_composite_test.sh b/apps/wolfsshd/test/sshd_mldsa_composite_test.sh new file mode 100755 index 000000000..419cdf5b0 --- /dev/null +++ b/apps/wolfsshd/test/sshd_mldsa_composite_test.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# sshd mldsa composite test + +PWD=`pwd` +cd ../../.. + +TEST_CLIENT="./examples/client/client" +USER=`whoami` +PRIVATE_KEY="./keys/hansel-key-ecc.der" +PUBLIC_KEY="./keys/hansel-key-ecc.pub" + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "./sshd_mldsa_composite_test.sh 127.0.0.1 22222" + exit 1 +fi + +set -e +echo "$TEST_CLIENT -c 'ls' -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -h \"$1\" -p \"$2\"" +$TEST_CLIENT -c 'ls' -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -h "$1" -p "$2" + +set +e + +cd $PWD +exit 0 diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 4f9d6c44a..13eb75e29 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -23,6 +23,7 @@ #endif #include +#include #include #include #include @@ -1406,18 +1407,19 @@ static int test_CheckPasswordHashUnix(void) #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ #ifdef WOLFSSL_BASE64_ENCODE -/* Build a mutable "ssh-rsa " line; WSTRTOK mutates in place. */ -static int BuildAuthKeysLine(const byte* key, word32 keySz, - char* lineOut, word32 lineOutSz) +/* Build a mutable " " line; WSTRTOK mutates in place. */ +static int BuildAuthKeysLineType(const char* type, const byte* key, + word32 keySz, char* lineOut, word32 lineOutSz) { - static const char prefix[] = "ssh-rsa "; - word32 prefixLen = (word32)(sizeof(prefix) - 1); + word32 typeLen = (word32)WSTRLEN(type); + word32 prefixLen = typeLen + 1; word32 b64Sz; if (lineOutSz <= prefixLen) { return WS_BUFFER_E; } - WMEMCPY(lineOut, prefix, prefixLen); + WMEMCPY(lineOut, type, typeLen); + lineOut[typeLen] = ' '; b64Sz = lineOutSz - prefixLen; if (Base64_Encode_NoNl(key, keySz, (byte*)lineOut + prefixLen, &b64Sz) != 0) { @@ -1431,7 +1433,266 @@ static int BuildAuthKeysLine(const byte* key, word32 keySz, return WS_SUCCESS; } -/* Negative-path coverage for CheckAuthKeysLine's ConstantCompare clause. */ +static int BuildAuthKeysLine(const byte* key, word32 keySz, + char* lineOut, word32 lineOutSz) +{ + return BuildAuthKeysLineType("ssh-rsa", key, keySz, lineOut, lineOutSz); +} + +/* Confirms every key-type string in CheckAuthKeysLine's allowedTypes[] table + * is recognized, guarding against the table and its recognition logic + * drifting out of sync as the ML-DSA/composite/cert #ifdef branches change. */ +static int test_CheckAuthKeysLineTypes(void) +{ + static const char* types[] = { + "ssh-rsa", + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + #ifdef WOLFSSH_CERTS + "x509v3-ssh-rsa", + "x509v3-ecdsa-sha2-nistp256", + "x509v3-ecdsa-sha2-nistp384", + "x509v3-ecdsa-sha2-nistp521", + #endif + #ifndef WOLFSSH_NO_MLDSA + #ifndef WOLFSSH_NO_MLDSA44 + "ssh-mldsa-44", + #endif + #ifndef WOLFSSH_NO_MLDSA65 + "ssh-mldsa-65", + #endif + #ifndef WOLFSSH_NO_MLDSA87 + "ssh-mldsa-87", + #endif + #ifdef WOLFSSH_CERTS + #ifndef WOLFSSH_NO_MLDSA44 + "x509v3-ssh-mldsa-44", + #endif + #ifndef WOLFSSH_NO_MLDSA65 + "x509v3-ssh-mldsa-65", + #endif + #ifndef WOLFSSH_NO_MLDSA87 + "x509v3-ssh-mldsa-87", + #endif + #endif + #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + "ssh-mldsa44-es256@wolfssl.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256@wolfssl.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384@wolfssl.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa44-ed25519@openssh.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa65-ed25519@wolfssl.com", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448@wolfssl.com", + #endif + }; + static const char keyAStr[] = "wolfssh-auth-key-test-A-AAAAAAA"; + const byte* keyA = (const byte*)keyAStr; + const word32 keySz = (word32)(sizeof(keyAStr) - 1); + char line[256]; + char lineCopy[256]; + word32 i; + int ret = WS_SUCCESS; + int rc; + + for (i = 0; i < (word32)(sizeof(types) / sizeof(types[0])); i++) { + ret = BuildAuthKeysLineType(types[i], keyA, keySz, line, sizeof(line)); + if (ret != WS_SUCCESS) { + Log(" CheckAuthKeysLine type %s: build failed.\n", types[i]); + return ret; + } + 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. */ + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), + keyA, keySz); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + return WS_FATAL_ERROR; + } + } + + /* 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, + sizeof(line)); + if (ret != WS_SUCCESS) { + return ret; + } + Log(" Testing scenario: unknown type is rejected."); + WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), keyA, keySz); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + return WS_FATAL_ERROR; + } + + return WS_SUCCESS; +} + +#ifndef WOLFSSH_NO_MLDSA +/* Builds a worst-case " " line, confirms it fits + * within MAX_LINE_SZ, and round-trips it through CheckAuthKeysLine(). */ +static int CheckAuthKeysLineMaxSzCase(const char* type, word32 keySz, + word32 maxLineSz) +{ + int ret = WS_SUCCESS; + int rc; + word32 lineBufSz = maxLineSz + 1; + byte* key = NULL; + char* line = NULL; + char* lineCopy = NULL; + + key = (byte*)WMALLOC(keySz, NULL, DYNTYPE_BUFFER); + line = (char*)WMALLOC(lineBufSz, NULL, DYNTYPE_BUFFER); + lineCopy = (char*)WMALLOC(lineBufSz, NULL, DYNTYPE_BUFFER); + if (key == NULL || line == NULL || lineCopy == NULL) { + ret = WS_MEMORY_E; + } + + if (ret == WS_SUCCESS) { + word32 i; + /* Non-repeating pattern so a truncation bug shows up as a + * mismatch, not accidental luck. */ + for (i = 0; i < keySz; i++) { + key[i] = (byte)(i * 31 + 7); + } + + ret = BuildAuthKeysLineType(type, key, keySz, line, lineBufSz); + } + + if (ret == WS_SUCCESS) { + word32 lineLen = (word32)WSTRLEN(line); + + Log(" Testing scenario: max-size %s (%u byte key, %u byte line) " + "fits within MAX_LINE_SZ (%u) and round-trips.", + type, keySz, lineLen, maxLineSz); + if (lineLen + 1 > maxLineSz) { + Log(" FAILED (line len %u exceeds MAX_LINE_SZ %u).\n", + lineLen + 1, maxLineSz); + ret = WS_FATAL_ERROR; + } + else { + WMEMCPY(lineCopy, line, lineLen + 1); + rc = CheckAuthKeysLine(lineCopy, lineLen, key, keySz); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + } + + if (key != NULL) { + WFREE(key, NULL, DYNTYPE_BUFFER); + } + if (line != NULL) { + WFREE(line, NULL, DYNTYPE_BUFFER); + } + if (lineCopy != NULL) { + WFREE(lineCopy, NULL, DYNTYPE_BUFFER); + } + + return ret; +} + +/* MAX_LINE_SZ is sized off the largest ML-DSA level, not the 32-byte + * dummy keys test_CheckAuthKeysLineTypes() uses; build a full-size key + * to actually catch a miscalculation there. */ +static int test_CheckAuthKeysLineMaxSz(void) +{ + int ret; + const char* type; + word32 keySz; + word32 maxLineSz = wolfsshd_test_MaxLineSz(); + +#if !defined(WOLFSSH_NO_MLDSA87) + keySz = WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ; + #if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + type = "ssh-mldsa87-es384@wolfssl.com"; + #elif defined(HAVE_ED448) + type = "ssh-mldsa87-ed448@wolfssl.com"; + #else + type = "ssh-mldsa-87"; + keySz = WC_MLDSA_87_PUB_KEY_SIZE; + #endif +#elif !defined(WOLFSSH_NO_MLDSA65) + keySz = WC_MLDSA_65_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ; + #if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + type = "ssh-mldsa65-es256@wolfssl.com"; + #elif !defined(WOLFSSH_NO_ED25519) && !defined(NO_SHA512) + type = "ssh-mldsa65-ed25519@wolfssl.com"; + #else + type = "ssh-mldsa-65"; + keySz = WC_MLDSA_65_PUB_KEY_SIZE; + #endif +#else + keySz = WC_MLDSA_44_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ; + #if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + type = "ssh-mldsa44-es256@wolfssl.com"; + #elif !defined(WOLFSSH_NO_ED25519) && !defined(NO_SHA512) + type = "ssh-mldsa44-ed25519@openssh.com"; + #else + type = "ssh-mldsa-44"; + keySz = WC_MLDSA_44_PUB_KEY_SIZE; + #endif +#endif + + ret = CheckAuthKeysLineMaxSzCase(type, keySz, maxLineSz); + +#if defined(WOLFSSH_CERTS) + /* WOLFSSH_CERTS adds MAX_LINE_SZ headroom for x509v3 composite-cert + * lines; cover that branch too, not just the plain-pubkey case. */ + if (ret == WS_SUCCESS) { + #if !defined(WOLFSSH_NO_MLDSA87) + type = "x509v3-ssh-mldsa-87"; + keySz = WC_MLDSA_87_PUB_KEY_SIZE + WC_MLDSA_87_SIG_SIZE + + COMPOSITE_MAX_TRAD_PUB_SZ + 1024; + #elif !defined(WOLFSSH_NO_MLDSA65) + type = "x509v3-ssh-mldsa-65"; + keySz = WC_MLDSA_65_PUB_KEY_SIZE + WC_MLDSA_65_SIG_SIZE + + COMPOSITE_MAX_TRAD_PUB_SZ + 1024; + #else + type = "x509v3-ssh-mldsa-44"; + keySz = WC_MLDSA_44_PUB_KEY_SIZE + WC_MLDSA_44_SIG_SIZE + + COMPOSITE_MAX_TRAD_PUB_SZ + 1024; + #endif + ret = CheckAuthKeysLineMaxSzCase(type, keySz, maxLineSz); + } +#endif /* WOLFSSH_CERTS */ + + return ret; +} +#endif /* !WOLFSSH_NO_MLDSA */ + +/* Negative-path coverage for CheckAuthKeysLine so mutation of the + * ConstantCompare clause (the only substantive bytewise check after the + * length comparison) does not survive the test suite. */ static int test_CheckAuthKeysLine(void) { int ret = WS_SUCCESS; @@ -3722,6 +3983,270 @@ static int test_ResolveAuthKeysPath(void) return ret; } +/* read an entire file into a heap buffer; *outSz is set to the file size. + * returns NULL on any failure */ +static byte* ReadWholeFile(const char* path, word32* outSz) +{ + FILE* f; + byte* buf = NULL; + long sz; + + f = fopen(path, "rb"); + if (f == NULL) { + return NULL; + } + if (fseek(f, 0, SEEK_END) != 0 || (sz = ftell(f)) < 0 || + fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + buf = (byte*)malloc((size_t)sz); + if (buf != NULL) { + if (fread(buf, 1, (size_t)sz, f) != (size_t)sz) { + free(buf); + buf = NULL; + } + } + fclose(f); + if (buf != NULL) { + *outSz = (word32)sz; + } + return buf; +} + +/* locate the repo's keys/ directory regardless of whether this binary is run + * from the repo root or from apps/wolfsshd/test/ */ +static int BuildKeyPath(const char* name, char* out, size_t outSz) +{ + static const char* candidates[] = { "keys/", "../../../keys/" }; + word32 i; + FILE* f; + + for (i = 0; i < (word32)(sizeof(candidates) / sizeof(candidates[0])); + i++) { + snprintf(out, outSz, "%s%s", candidates[i], name); + f = fopen(out, "rb"); + if (f != NULL) { + fclose(f); + return WS_SUCCESS; + } + } + return WS_FATAL_ERROR; +} + +/* Regression coverage for wolfSSHD_DetectPrivKeyFormat(), the host-key + * format auto-detection SetupCTX() relies on to load PEM-armored OpenSSH + * keys, raw binary openssh-key-v1 blobs (including composite ML-DSA host + * keys, which are only ever stored in that raw form), PKCS#8 PEM keys + * (e.g. ML-DSA), and traditional PEM/DER keys. */ +static int test_DetectPrivKeyFormat(void) +{ + typedef struct { + const char* desc; + const char* file; + int wantFormat; + /* Expected wc_KeyPemToDer() outcome (1 = succeeds, keyDer non-NULL; + * 0 = fails, keyDer NULL); pins down which branch each case hits. */ + int wantKeyDerNonNull; + } DPK_CASE; + static const DPK_CASE cases[] = { + { "PEM-armored OpenSSH key", "id_ecdsa", WOLFSSH_FORMAT_OPENSSH, 0 }, + { "raw binary openssh-key-v1 composite ML-DSA key", + "server-key-mldsa44ed25519", WOLFSSH_FORMAT_OPENSSH, 0 }, + { "PEM traditional key decodes to DER/ASN1", "server-key-ecc.pem", + WOLFSSH_FORMAT_ASN1, 1 }, + { "un-armored raw DER key falls through to ASN1", + "server-key-mldsa44.der", WOLFSSH_FORMAT_ASN1, 0 }, + { "PKCS#8 PEM ML-DSA key decodes to DER/ASN1 without PKCS8 " + "stripping", "server-key-mldsa44.pem", WOLFSSH_FORMAT_ASN1, 1 }, + }; + word32 i; + int ret = WS_SUCCESS; + byte dummy = 0; + byte* badKeyDer = NULL; + byte* badPrivBuf = NULL; + word32 badPrivBufSz = 0; + int badGot; + + /* A 0-byte host key file (or otherwise bad arguments) must be rejected + * without touching the out-params, matching the empty-file case + * getBufferFromFile() can hand back. Poison out-params with sentinels + * first, so the NULL/0 checks below catch a skipped reset instead of + * matching by coincidence. */ + badKeyDer = (byte*)&dummy; + badPrivBuf = (byte*)&dummy; + badPrivBufSz = 0xDEADBEEF; + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, 0, NULL, &badKeyDer, + &badPrivBuf, &badPrivBufSz); + Log(" Testing scenario: 0-length buffer. %s\n", + (badGot == WS_BAD_ARGUMENT && badKeyDer == NULL && + badPrivBuf == NULL && badPrivBufSz == 0) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT || badKeyDer != NULL || + badPrivBuf != NULL || badPrivBufSz != 0) { + return WS_FATAL_ERROR; + } + + badGot = wolfSSHD_DetectPrivKeyFormat(NULL, sizeof(dummy), NULL, + &badKeyDer, &badPrivBuf, &badPrivBufSz); + Log(" Testing scenario: NULL data pointer. %s\n", + (badGot == WS_BAD_ARGUMENT) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT) { + return WS_FATAL_ERROR; + } + + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, sizeof(dummy), NULL, NULL, + &badPrivBuf, &badPrivBufSz); + Log(" Testing scenario: NULL keyDer pointer. %s\n", + (badGot == WS_BAD_ARGUMENT) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT) { + return WS_FATAL_ERROR; + } + + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, sizeof(dummy), NULL, + &badKeyDer, NULL, &badPrivBufSz); + Log(" Testing scenario: NULL privBuf pointer. %s\n", + (badGot == WS_BAD_ARGUMENT) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT) { + return WS_FATAL_ERROR; + } + + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, sizeof(dummy), NULL, + &badKeyDer, &badPrivBuf, NULL); + Log(" Testing scenario: NULL privBufSz pointer. %s\n", + (badGot == WS_BAD_ARGUMENT) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT) { + return WS_FATAL_ERROR; + } + + for (i = 0; i < (word32)(sizeof(cases) / sizeof(cases[0])); i++) { + char path[128]; + byte* data; + word32 dataSz = 0; + byte* keyDer = NULL; + byte* privBuf = NULL; + word32 privBufSz = 0; + int gotFormat; + + if (BuildKeyPath(cases[i].file, path, sizeof(path)) != WS_SUCCESS) { + Log(" Testing scenario: %s. FAILED (couldn't locate %s)\n", + cases[i].desc, cases[i].file); + return WS_FATAL_ERROR; + } + + data = ReadWholeFile(path, &dataSz); + if (data == NULL) { + Log(" Testing scenario: %s. FAILED (couldn't read %s)\n", + cases[i].desc, path); + return WS_FATAL_ERROR; + } + + gotFormat = wolfSSHD_DetectPrivKeyFormat(data, dataSz, NULL, &keyDer, + &privBuf, &privBufSz); + + Log(" Testing scenario: %s. %s\n", cases[i].desc, + (gotFormat == cases[i].wantFormat) ? "PASSED" : "FAILED"); + if (gotFormat != cases[i].wantFormat) { + ret = WS_FATAL_ERROR; + } + + Log(" Testing scenario: %s wc_KeyPemToDer branch. %s\n", + cases[i].desc, + ((keyDer != NULL) == (cases[i].wantKeyDerNonNull != 0)) ? + "PASSED" : "FAILED"); + if ((keyDer != NULL) != (cases[i].wantKeyDerNonNull != 0)) { + ret = WS_FATAL_ERROR; + } + + if (gotFormat >= 0) { + int privOk = 0; + if (keyDer != NULL && privBuf == keyDer && privBufSz > 0) { + privOk = 1; + } else if (keyDer == NULL && privBuf == data && privBufSz == dataSz) { + privOk = 1; + } + Log(" Testing scenario: %s privBuf correctness. %s\n", + cases[i].desc, privOk ? "PASSED" : "FAILED"); + if (!privOk) { + ret = WS_FATAL_ERROR; + } + } + + if (keyDer != NULL) { + WFREE(keyDer, NULL, DYNTYPE_SSHD); + } + free(data); + if (ret != WS_SUCCESS) { + return ret; + } + } + + /* Synthetic case: wc_KeyPemToDer() succeeds but the decoded body starts + * with the openssh-key-v1 magic -- forces the "PEM-decoded result may + * still be OpenSSH binary" path no file-based case above reaches. */ + { + /* base64 of "openssh-key-v1\0PADPADPADPADPAD" */ + static const char pemOpenSshBody[] = + "-----BEGIN PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAUEFEUEFEUEFEUEFEUEFE\n" + "-----END PRIVATE KEY-----\n"; + byte* keyDer = NULL; + byte* privBuf = NULL; + word32 privBufSz = 0; + int gotFormat; + + gotFormat = wolfSSHD_DetectPrivKeyFormat( + (byte*)pemOpenSshBody, (word32)(sizeof(pemOpenSshBody) - 1), + NULL, &keyDer, &privBuf, &privBufSz); + + Log(" Testing scenario: PEM decodes to an OpenSSH blob. %s\n", + (gotFormat == WOLFSSH_FORMAT_OPENSSH && keyDer != NULL) ? + "PASSED" : "FAILED"); + if (gotFormat != WOLFSSH_FORMAT_OPENSSH || keyDer == NULL) { + ret = WS_FATAL_ERROR; + } + + if (keyDer != NULL) { + WFREE(keyDer, NULL, DYNTYPE_SSHD); + } + if (ret != WS_SUCCESS) { + return ret; + } + } + + /* Synthetic case: Corrupt/unsupported PEM buffer that fails wc_KeyPemToDer + * and does not start with OpenSSH magic, nor raw DER (0x30). */ + { + static const char corruptPem[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "not valid base64 !!!\n" + "-----END RSA PRIVATE KEY-----\n"; + byte* keyDer = NULL; + byte* privBuf = NULL; + word32 privBufSz = 0; + int gotFormat; + + gotFormat = wolfSSHD_DetectPrivKeyFormat( + (byte*)corruptPem, (word32)(sizeof(corruptPem) - 1), + NULL, &keyDer, &privBuf, &privBufSz); + + Log(" Testing scenario: Corrupt/unsupported PEM buffer. %s\n", + (gotFormat == WS_BAD_FILE_E && keyDer == NULL) ? + "PASSED" : "FAILED"); + if (gotFormat != WS_BAD_FILE_E || keyDer != NULL) { + ret = WS_FATAL_ERROR; + } + + if (keyDer != NULL) { + WFREE(keyDer, NULL, DYNTYPE_SSHD); + } + if (ret != WS_SUCCESS) { + return ret; + } + } + + return ret; +} + const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_ParseConfigLine), @@ -3747,8 +4272,13 @@ const TEST_CASE testCases[] = { TEST_DECL(test_OpenSecureFile), TEST_DECL(test_ConfigSavePID), #endif + TEST_DECL(test_DetectPrivKeyFormat), #ifdef WOLFSSL_BASE64_ENCODE TEST_DECL(test_CheckAuthKeysLine), + TEST_DECL(test_CheckAuthKeysLineTypes), + #ifndef WOLFSSH_NO_MLDSA + TEST_DECL(test_CheckAuthKeysLineMaxSz), + #endif #endif #if defined(WOLFSSL_BASE64_ENCODE) && !defined(_WIN32) TEST_DECL(test_SearchForPubKey), diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 54031b6f8..044fcb481 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -349,6 +349,7 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, byte** banner) { int ret = WS_SUCCESS; + byte* keyDer = NULL; byte* privBuf = NULL; word32 privBufSz = 0; void* heap = NULL; @@ -411,20 +412,13 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } if (ret == WS_SUCCESS) { - /* Host keys may be PEM or DER. Detect by content: a DER key is - * an ASN.1 SEQUENCE (leading 0x30); anything else is treated as - * PEM text and decoded with wc_KeyPemToDer(), which handles - * PKCS#1, SEC1 and PKCS#8 "PRIVATE KEY" bodies. - * - * The previous code used wc_PemToDer(..., PRIVATEKEY_TYPE, ...), - * which only recognizes the classic "RSA/EC PRIVATE KEY" PEM - * headers. On a PKCS#8 body (how ML-DSA host keys are emitted) - * it returns *success* but yields a malformed body (leading - * 0x04, not a 0x30 SEQUENCE), which - * wolfSSH_CTX_UsePrivateKey_buffer() then rejects with - * WS_BAD_FILETYPE_E, so ML-DSA PEM host keys could not load. */ - byte* keyDer = NULL; - + /* Host keys may be OpenSSH, PEM, or DER. + * wolfSSHD_DetectPrivKeyFormat() uses wc_KeyPemToDer() (not + * wc_PemToDer(..., PRIVATEKEY_TYPE, ...), which unwraps + * PKCS#8 via ToTraditional() and mangles key types with no + * traditional DER form, e.g. ML-DSA) and also detects the + * OpenSSH private-key format, used by composite ML-DSA host + * keys generated in that format. */ if (dataSz == 0) { /* An empty (0-byte) file passes the NULL check above but * carries no key material. Handle it explicitly as a file @@ -434,45 +428,44 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Host key file is empty."); ret = WS_BAD_FILE_E; } - else if (data[0] == 0x30) { - privBuf = data; - privBufSz = dataSz; - } else { - keyDer = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD); - if (keyDer == NULL) { + int keyFormat = wolfSSHD_DetectPrivKeyFormat(data, dataSz, + heap, &keyDer, &privBuf, &privBufSz); + + if (keyFormat == WS_MEMORY_E) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Out of memory reading host private key."); ret = WS_MEMORY_E; } + else if (keyFormat < 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Host private key file is invalid."); + ret = WS_BAD_FILE_E; + } + else if (keyFormat == WOLFSSH_FORMAT_OPENSSH) { + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " + "key as OpenSSH format."); + } else { - int keyDerSz = wc_KeyPemToDer(data, dataSz, keyDer, - (int)dataSz, NULL); - if (keyDerSz <= 0) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to convert " - "host private key from PEM."); - ret = WS_BAD_FILE_E; - } - else { - privBuf = keyDer; - privBufSz = (word32)keyDerSz; - } + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " + "key as DER format."); } - } - if (ret == WS_SUCCESS - && wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, - privBufSz, WOLFSSH_FORMAT_ASN1) < 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to use host private key."); - ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && + wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, + privBufSz, keyFormat) < 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to use host private key."); + ret = WS_BAD_ARGUMENT; + } } if (keyDer != NULL) { WS_FORCEZERO(keyDer, dataSz); WFREE(keyDer, heap, DYNTYPE_SSHD); } - /* data held the raw private key — the DER bytes, or the PEM - * text decoded into keyDer above. Zeroize before freeing so key - * material does not linger in the heap after use. */ + /* data is the key material itself for raw DER/OpenSSH + * input (privBuf aliases it directly, no copy). */ WS_FORCEZERO(data, dataSz); freeBufferFromFile(data, heap); } diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 92ea43bbf..81cd421b6 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1851,8 +1851,92 @@ static int load_key_mldsa87(byte* buf, word32 bufSz) } #endif /* WOLFSSH_NO_MLDSA87 */ - #ifndef WOLFSSH_NO_MLDSA +/* composite key buffer must be sized from the file, not a fixed constant */ +static int LoadMlDsaCompositeHostKey(WOLFSSH_CTX* ctx, + const char* fileName, const char* label) +{ +#ifndef NO_FILESYSTEM + byte* compBuf = NULL; + word32 compBufSz = 0; + word32 allocSz; + word32 compSz; + + load_file(fileName, NULL, &compBufSz); + if (compBufSz == 0) { + fprintf(stderr, "Couldn't find size of %s key file.\n", label); + return -1; + } + allocSz = compBufSz; + compBuf = (byte*)WMALLOC(allocSz, NULL, 0); + if (compBuf == NULL) { + fprintf(stderr, "Couldn't allocate %s key buffer.\n", label); + return -1; + } + compSz = load_file(fileName, compBuf, &compBufSz); + if (compSz == 0) { + wc_ForceZero(compBuf, allocSz); + WFREE(compBuf, NULL, 0); + fprintf(stderr, "Couldn't load %s key file.\n", label); + return -1; + } + if (wolfSSH_CTX_UsePrivateKey_buffer(ctx, compBuf, compSz, + WOLFSSH_FORMAT_OPENSSH) < 0) { + wc_ForceZero(compBuf, allocSz); + WFREE(compBuf, NULL, 0); + fprintf(stderr, "Couldn't use %s key buffer.\n", label); + return -1; + } + wc_ForceZero(compBuf, allocSz); + WFREE(compBuf, NULL, 0); + return 0; +#else + (void)ctx; (void)fileName; + fprintf(stderr, "Couldn't load %s key: no filesystem.\n", label); + return -1; +#endif /* NO_FILESYSTEM */ +} + +typedef struct { + const char* substr; + const char* fileName; + const char* label; +} MlDsaCompositeEntry; + +/* NULL-terminated so the table is never empty if ECDSA and Ed25519/Ed448 + * are both disabled */ +static const MlDsaCompositeEntry mldsaCompositeEntries[] = { +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { "mldsa44-ed25519", "./keys/server-key-mldsa44ed25519", + "ML-DSA-44+Ed25519" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + { "mldsa44-es256", "./keys/server-key-mldsa44es256", + "ML-DSA-44+ES256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { "mldsa65-ed25519", "./keys/server-key-mldsa65ed25519", + "ML-DSA-65+Ed25519" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + { "mldsa65-es256", "./keys/server-key-mldsa65es256", + "ML-DSA-65+ES256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + { "mldsa87-ed448", "./keys/server-key-mldsa87ed448", + "ML-DSA-87+Ed448" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + { "mldsa87-es384", "./keys/server-key-mldsa87es384", + "ML-DSA-87+ES384" }, +#endif + { NULL, NULL, NULL } +}; + static int LoadMlDsaHostKeys(WOLFSSH_CTX* ctx, const char* keyList) { byte* mldsaBuf; @@ -3363,13 +3447,59 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) * unconditionally would force mldsa negotiation on non-mldsa tests. */ #ifndef WOLFSSH_NO_MLDSA if (keyList != NULL && WSTRSTR(keyList, "mldsa") != NULL) { - if (LoadMlDsaHostKeys(ctx, keyList) != 0) { + int mldsaErr = 0; + int mldsaMatched = 0; + + /* skip LoadMlDsaHostKeys() for a purely composite keyList; it + * only knows plain "mldsa-NN" names and would abort */ + if (WSTRSTR(keyList, "mldsa-44") != NULL || + WSTRSTR(keyList, "mldsa-65") != NULL || + WSTRSTR(keyList, "mldsa-87") != NULL) { + mldsaMatched = 1; + if (LoadMlDsaHostKeys(ctx, keyList) != 0) { + mldsaErr = 1; + } + } + + if (mldsaErr) { #ifdef WOLFSSH_SMALL_STACK wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); WFREE(keyLoadBuf, NULL, 0); #endif ES_ERROR("Error loading ML-DSA host keys.\n"); } + else { + word32 mldsaIdx; + + for (mldsaIdx = 0; + mldsaCompositeEntries[mldsaIdx].substr != NULL; + mldsaIdx++) { + const MlDsaCompositeEntry* entry = + &mldsaCompositeEntries[mldsaIdx]; + + if (WSTRSTR(keyList, entry->substr) != NULL) { + mldsaMatched = 1; + if (LoadMlDsaCompositeHostKey(ctx, entry->fileName, + entry->label) != 0) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif + ES_ERROR("Error loading %s host key.\n", + entry->label); + } + } + } + + if (!mldsaMatched) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif + ES_ERROR("ML-DSA key list '%s' matched no supported " + "level.\n", keyList); + } + } } #endif /* WOLFSSH_NO_MLDSA */ diff --git a/keys/include.am b/keys/include.am index 99dcb4ac0..476678885 100644 --- a/keys/include.am +++ b/keys/include.am @@ -25,8 +25,12 @@ EXTRA_DIST+= \ keys/id_ecdsa keys/id_ecdsa.pub keys/id_rsa keys/id_rsa.pub \ keys/renewcerts.sh keys/renewcerts.cnf \ keys/server-key-ed25519.der keys/server-key-ed25519.pem \ - keys/server-key-mldsa44.der keys/server-key-mldsa65.der \ - keys/server-key-mldsa87.der \ + keys/server-key-mldsa44.der keys/server-key-mldsa44.pem \ + keys/server-key-mldsa65.der \ + keys/server-key-mldsa87.der keys/server-key-mldsa44ed25519 \ + keys/server-key-mldsa44es256 keys/server-key-mldsa65ed25519 \ + keys/server-key-mldsa65es256 keys/server-key-mldsa87ed448 \ + keys/server-key-mldsa87es384 \ keys/renew-ossh-certs.sh \ keys/ossh-ca keys/ossh-ca.pub \ keys/ossh-ca-rsa keys/ossh-ca-rsa.pub \ diff --git a/keys/server-key-mldsa44.pem b/keys/server-key-mldsa44.pem new file mode 100644 index 000000000..313c9767d --- /dev/null +++ b/keys/server-key-mldsa44.pem @@ -0,0 +1,84 @@ +-----BEGIN PRIVATE KEY----- +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 +oexp1D0GP2IqStVF82Fvng== +-----END PRIVATE KEY----- diff --git a/keys/server-key-mldsa44ed25519 b/keys/server-key-mldsa44ed25519 new file mode 100644 index 000000000..ead7eed17 Binary files /dev/null and b/keys/server-key-mldsa44ed25519 differ diff --git a/keys/server-key-mldsa44es256 b/keys/server-key-mldsa44es256 new file mode 100644 index 000000000..4eeace1f0 --- /dev/null +++ b/keys/server-key-mldsa44es256 @@ -0,0 +1,64 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAFhgAAAB1z +c2gtbWxkc2E0NC1lczI1NkB3b2xmc3NsLmNvbQAABWEkOjKuVYKNHxvgtY0n6XfC +l1E3c9mT5dEySN/+lCuota22IzFfvP/R3sivUMueGK4OCOQaHghjqdeI7mii/R4N +z+V/xF9lB4THcYGKjOI8GDfPTrMJxPCemxM8Cywwz45NsPvVRydvgDMN3JC2Ulg6 +V7ZFJi55zz5o+JutRmd/UCMiMfAj3/VAOi2jb2m68HX4PYX737pCYiR88xNaET+0 +BCZwmUSOAqAMdHqlcVQI4s6CxBKsZSBw1YagkAe9aBaeyCG1DraooRky1erWXoaA +JuPASo+99A3SruH1jND6L0Vwn0iFHE5UynnmueRDPJPOpiB5jwdJ8d/SIb8F5oVq +trxzdoc+FDSKl9f+b1mclsXDR76fk9zzmYLSipgfFzPFaLizRTkqwzeVDdWaMqvE +Y/nuofqmPq4VaN998DGblma45G4PGRqlfxDFTiHplhmoxnrM+1BvpNYvAZJtV86n +MIQpU8pHQPZ5rZv2ZrepYHTO2UMrliXh7MAGPF4AO7pKJpQkF7mtbMHk7ABimXjR +ewR2QKsxCQC6yAuCrJKAUcRKNrdQaBAoavWizOejD8ZCU6HWHi/MqGzONJ0Gim+Q +Tn54L9sbd0riJv/meiv3icZqAxNY9uYqu6H7A26bn6yIrD9YIfqc3XRmgXOJLCG0 +rgvfQDpXa9c3gwtgdQH0IRuR0vr3IgT2U670L9KNPqn0orLriYrW2EKynENt8iX0 +JMGVZqZiAivRO/a9DhqTSlRczQaCAfkjzK6isPet8sWqhwxwwrKQmng8ZHuagVBQ +MceP0EgG7XybsOY/IFaTUmLPSOjZWxuiLHRU8f6f1ukpTdDKebK6jGT8pBp8e+Ju +/ycBaCqg8cDBxvi62vy+sYXMK6ZsJ5jhPzGWdwKiykYyRVB7DhRha4Dx0SRhhBdD +fJP5BxHLCWc28qfVsU4pI9siASikIxGgyYfKcLyyiOeCiLMfSTLkJi1Trjon77He +nnY2Q2K9ijatPnOt8Z+rM6lw8DfoLX9c2DIdsR9YBTyEtUOFswpRWjjpegBd5Eu/ +XZKdouaunogdfUzG7mfXw7hS1/pVLO4adxhROHT0r496mTKgygI+EsCKAU5ZU9CU +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/server-key-mldsa65ed25519 b/keys/server-key-mldsa65ed25519 new file mode 100644 index 000000000..cbaef3631 --- /dev/null +++ b/keys/server-key-mldsa65ed25519 @@ -0,0 +1,89 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/server-key-mldsa65es256 b/keys/server-key-mldsa65es256 new file mode 100644 index 000000000..bf1fbb49e --- /dev/null +++ b/keys/server-key-mldsa65es256 @@ -0,0 +1,91 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +3VFth3LT3AAAAAABAg== +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/server-key-mldsa87ed448 b/keys/server-key-mldsa87ed448 new file mode 100644 index 000000000..398aa7ed9 --- /dev/null +++ b/keys/server-key-mldsa87ed448 @@ -0,0 +1,118 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +AAAAAAE= +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/server-key-mldsa87es384 b/keys/server-key-mldsa87es384 new file mode 100644 index 000000000..563b81edf --- /dev/null +++ b/keys/server-key-mldsa87es384 @@ -0,0 +1,119 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/src/internal.c b/src/internal.c index ea2bfa799..74ed20331 100644 --- a/src/internal.c +++ b/src/internal.c @@ -65,6 +65,22 @@ #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(WC_MLDSA_FIXED_ARRAY) && \ + (defined(WC_MLDSA_CACHE_MATRIX_A) || \ + defined(WC_MLDSA_CACHE_PRIV_VECTORS) || \ + defined(WC_MLDSA_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." + #endif + + #endif #ifdef NO_INLINE @@ -510,6 +526,9 @@ const char* GetErrorString(int err) case WS_MLDSA_E: return "ML-DSA error"; + case WS_ED448_E: + return "Ed448 buffer error"; + case WS_AUTH_PENDING: return "userauth is still pending (callback would block)"; @@ -525,7 +544,6 @@ const char* GetErrorString(int err) #endif } - static int wsHighwater(byte dir, void* ctx) { int ret = WS_SUCCESS; @@ -1017,6 +1035,28 @@ static const char cannedKexAlgoNames[] = /* ML-DSA listed first (post-quantum priority), then ECDSA, ED25519, RSA. */ static const char cannedKeyAlgoNames[] = +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448@wolfssl.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384@wolfssl.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa65-ed25519@wolfssl.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256@wolfssl.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa44-ed25519@openssh.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + "ssh-mldsa44-es256@wolfssl.com," +#endif #ifndef WOLFSSH_NO_MLDSA87 "ssh-mldsa-87," #endif @@ -1096,6 +1136,28 @@ 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[] = +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448@wolfssl.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384@wolfssl.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa65-ed25519@wolfssl.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256@wolfssl.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa44-ed25519@openssh.com," +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + "ssh-mldsa44-es256@wolfssl.com," +#endif #ifndef WOLFSSH_NO_MLDSA87 "ssh-mldsa-87," #endif @@ -1770,6 +1832,21 @@ void wolfSSH_KEY_clean(WS_KeySignature* key) key->keyId == ID_X509V3_MLDSA87) { wc_MlDsaKey_Free(&key->ks.mldsa.key); } + else if (key->keyId == ID_MLDSA44_ES256 || + key->keyId == ID_MLDSA65_ES256 || + key->keyId == ID_MLDSA87_ES384 || + key->keyId == ID_MLDSA44_ED25519 || + key->keyId == ID_MLDSA65_ED25519 || + key->keyId == ID_MLDSA87_ED448) { + CompositeParams params; + wc_MlDsaKey_Free(&key->ks.mldsa_composite.mldsa); + if (WS_GetCompositeParams(key->keyId, ¶ms) == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + if (ops != NULL) { + ops->free(&key->ks.mldsa_composite.trad); + } + } + } #endif else if (key->keyId == ID_ECDSA_SHA2_NISTP256 || key->keyId == ID_ECDSA_SHA2_NISTP384 || @@ -2007,6 +2084,78 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, /* The OpenSSH binary key-format decoders (GetOpenSshKey, * IdentifyOpenSshKey, and their helpers) live in src/ossh.c. */ +/* + * Finds the OPENSSH PRIVATE KEY markers and base64-decodes between them. + * Shared by DoOpenSshKey() and wolfSSH_ProcessBuffer() so the two copies + * can't drift apart. + * + * @param in PEM buffer starting at the begin marker + * @param inSz size of in[] + * @param out receives the decoded bytes + * @param outSz in: capacity of out[]; out: decoded length + * @return WS_SUCCESS, or WS_PARSE_E if the markers or decode fail + */ +int WS_StripOpenSshPem(const byte* in, word32 inSz, byte* out, word32* outSz) +{ + static const char* beginMarker = "-----BEGIN OPENSSH PRIVATE KEY-----"; + static const char* endMarker = "-----END OPENSSH PRIVATE KEY-----"; + word32 beginSz = (word32)WSTRLEN(beginMarker); + word32 endSz = (word32)WSTRLEN(endMarker); + const char* footer; + const byte* b64; + word32 b64Sz; + + /* Reject buffers too small to hold both markers. Without this guard + * the subtraction used to locate the base64 region underflows inSz. */ + if (inSz <= beginSz + endSz) { + return WS_PARSE_E; + } + if (WMEMCMP(in, beginMarker, beginSz) != 0) { + return WS_PARSE_E; + } + footer = WSTRNSTR((const char*)in + beginSz, endMarker, inSz - beginSz); + if (footer == NULL) { + return WS_PARSE_E; + } + + b64 = in + beginSz; + b64Sz = (word32)(footer - (const char*)b64); + + return (Base64_Decode(b64, b64Sz, out, outSz) == 0) ? + WS_SUCCESS : WS_PARSE_E; +} + +#ifndef WOLFSSH_NO_MLDSA +/* Inits both halves of a composite key pair; mldsaInit/tradInit track + * which succeeded so the caller can clean up correctly on failure. */ +int InitCompositeKeyPair(const CompositeParams* params, + MlDsaKey* mldsa, void* tradKey, const CompositeTradOps* ops, + void* heap, int* mldsaInit, int* tradInit) +{ + int ret; + + *mldsaInit = 0; + *tradInit = 0; + + ret = wc_MlDsaKey_Init(mldsa, heap, INVALID_DEVID); + if (ret == 0) { + *mldsaInit = 1; + ret = wc_MlDsaKey_SetParams(mldsa, params->mldsaLevel); + } + if (ret == 0) { + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->init(tradKey, heap); + if (ret == 0) *tradInit = 1; + } + } + + return ret; +} +#endif /* WOLFSSH_NO_MLDSA */ + #ifdef WOLFSSH_CERTS /* @@ -2483,6 +2632,9 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, heap = ctx->heap; + if (format == WOLFSSH_FORMAT_OPENSSH && type != BUFTYPE_PRIVKEY) + return WS_UNIMPLEMENTED_E; + if (format == WOLFSSH_FORMAT_ASN1 || format == WOLFSSH_FORMAT_RAW) { if (in[0] != 0x30) return WS_BAD_FILETYPE_E; @@ -2492,6 +2644,27 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, WMEMCPY(der, in, inSz); derSz = inSz; } + else if (format == WOLFSSH_FORMAT_OPENSSH) { + der = (byte*)WMALLOC(inSz, heap, dynamicType); + if (der == NULL) + return WS_MEMORY_E; + /* Strip the PEM wrapper so IdentifyOpenSshKey sees raw binary; + * mirrors DoOpenSshKey(), since wc_KeyPemToDer() doesn't know this. */ + if (inSz >= 5 && WMEMCMP(in, "-----", 5) == 0) { + word32 derOutSz = inSz; + + if (WS_StripOpenSshPem(in, inSz, der, &derOutSz) != WS_SUCCESS) { + WS_FORCEZERO(der, inSz); + WFREE(der, heap, dynamicType); + return WS_BAD_FILE_E; + } + derSz = derOutSz; + } + else { + WMEMCPY(der, in, inSz); + derSz = inSz; + } + } else if (format == WOLFSSH_FORMAT_PEM) { /* The der size will be smaller than the pem size. */ der = (byte*)WMALLOC(inSz, heap, dynamicType); @@ -2533,7 +2706,10 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, /* Maybe decrypt */ if (type == BUFTYPE_PRIVKEY) { - ret = IdentifyAsn1Key(der, derSz, 1, ctx->heap, NULL); + if (format == WOLFSSH_FORMAT_OPENSSH) + ret = IdentifyOpenSshKey(der, derSz, ctx->heap); + else + ret = IdentifyAsn1Key(der, derSz, 1, ctx->heap, NULL); if (ret < 0) { if (der != NULL) { WS_FORCEZERO(der, derSz); @@ -2542,6 +2718,19 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, return ret; } keyId = (byte)ret; + /* Only composite parsers can walk the stored openssh-key-v1 + * envelope; reject other key types now instead of at handshake. */ + if (format == WOLFSSH_FORMAT_OPENSSH +#ifndef WOLFSSH_NO_MLDSA + && keyId != ID_MLDSA44_ES256 && keyId != ID_MLDSA65_ES256 && + keyId != ID_MLDSA87_ES384 && keyId != ID_MLDSA44_ED25519 && + keyId != ID_MLDSA65_ED25519 && keyId != ID_MLDSA87_ED448 +#endif + ) { + WS_FORCEZERO(der, derSz); + WFREE(der, heap, dynamicType); + return WS_UNIMPLEMENTED_E; + } ret = SetHostPrivateKey(ctx, keyId, der, derSz, dynamicType); } #ifdef WOLFSSH_CERTS @@ -2917,6 +3106,28 @@ static const NameIdPair NameIdMap[] = { #ifndef WOLFSSH_NO_MLDSA44 { ID_MLDSA44, TYPE_KEY, "ssh-mldsa-44" }, #endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + { ID_MLDSA44_ES256, TYPE_KEY, "ssh-mldsa44-es256@wolfssl.com" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + { ID_MLDSA65_ES256, TYPE_KEY, "ssh-mldsa65-es256@wolfssl.com" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + { ID_MLDSA87_ES384, TYPE_KEY, "ssh-mldsa87-es384@wolfssl.com" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { ID_MLDSA44_ED25519, TYPE_KEY, "ssh-mldsa44-ed25519@openssh.com" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { ID_MLDSA65_ED25519, TYPE_KEY, "ssh-mldsa65-ed25519@wolfssl.com" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + { ID_MLDSA87_ED448, TYPE_KEY, "ssh-mldsa87-ed448@wolfssl.com" }, +#endif #ifndef WOLFSSH_NO_MLDSA65 { ID_MLDSA65, TYPE_KEY, "ssh-mldsa-65" }, #endif @@ -4162,6 +4373,28 @@ static const byte cannedKeyAlgoClient[] = { #endif /* WOLFSSH_NO_SSH_RSA_SHA1 */ #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ #endif /* WOLFSSH_CERTS */ +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ID_MLDSA87_ED448, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + ID_MLDSA87_ES384, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ID_MLDSA65_ED25519, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + ID_MLDSA65_ES256, +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ID_MLDSA44_ED25519, +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + ID_MLDSA44_ES256, +#endif #ifndef WOLFSSH_NO_MLDSA87 ID_MLDSA87, #endif @@ -4680,14 +4913,17 @@ static int IsKexMatchError(int ret) ret == WS_MATCH_ENC_ALGO_E || ret == WS_MATCH_MAC_ALGO_E; } +/* Headroom for DoKexInit()'s decoded name-lists; bump if a future + * algorithm addition pushes a canned list close to this limit. */ +#define WOLFSSH_KEXINIT_ID_LIST_MAX 32 static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) { int ret = WS_SUCCESS; int side = WOLFSSH_ENDPOINT_SERVER; byte algoId; - byte list[24] = {ID_NONE}; - byte cannedList[24] = {ID_NONE}; + byte list[WOLFSSH_KEXINIT_ID_LIST_MAX] = {ID_NONE}; + byte cannedList[WOLFSSH_KEXINIT_ID_LIST_MAX] = {ID_NONE}; byte kexIdGuess = ID_NONE; byte pubKeyIdGuess = ID_NONE; byte kexPacketFollows = 0; @@ -5356,7 +5592,9 @@ struct wolfSSH_sigKeyBlock { byte useEcc:1; byte useMlDsa:1; byte useEd25519:1; + byte useMlDsaComposite:1; byte keyAllocated:1; + byte pubKeyId; word32 keySz; union { #ifndef WOLFSSH_NO_RSA @@ -5378,6 +5616,21 @@ struct wolfSSH_sigKeyBlock { struct { ed25519_key key; } ed25519; +#endif +#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; }; @@ -5570,6 +5823,30 @@ static int ParseEd25519PubKey(WOLFSSH *ssh, } #endif +#ifndef WOLFSSH_NO_MLDSA +struct wolfSSH_sigKeyBlockFull; + +static int VerifyMlDsaComposite(byte keyId, void* heap, + MlDsaKey* mldsa, void* tradKey, + const byte* sig, word32 sigSz, + const byte* msg, word32 msgSz); +static int ParseMlDsaCompositePubKey(WOLFSSH* ssh, + struct wolfSSH_sigKeyBlock* sigKeyBlock_ptr, + byte* pubKey, word32 pubKeySz, byte keyId); +static int SignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, + struct wolfSSH_sigKeyBlockFull *sigKey); +static int PrepareUserAuthRequestMlDsaComposite(WOLFSSH* ssh, word32* payloadSz, + const WS_UserAuthData* authData, WS_KeySignature* keySig); +static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + byte* output, word32* idx, + const WS_UserAuthData* authData, + const byte* sigStart, word32 sigStartIdx, + WS_KeySignature* keySig); +static int DoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + WS_UserAuthData_PublicKey* pk, WS_UserAuthData* authData, + byte keyId, word32 pubKeyBlobSz); +#endif + #ifndef WOLFSSH_NO_MLDSA /* Parse out a RAW ML-DSA public key from buffer */ static int ParseMlDsaPubKey(WOLFSSH* ssh, @@ -5958,6 +6235,17 @@ static int ParsePubKey(WOLFSSH *ssh, pubKeySz, ssh->handshake->pubKeyId); break; #endif + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: + case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: + sigKeyBlock_ptr->useMlDsaComposite = 1; + sigKeyBlock_ptr->pubKeyId = ssh->handshake->pubKeyId; + ret = ParseMlDsaCompositePubKey(ssh, sigKeyBlock_ptr, pubKey, + pubKeySz, ssh->handshake->pubKeyId); + break; #endif default: @@ -6025,6 +6313,18 @@ static void FreePubKey(struct wolfSSH_sigKeyBlock *p) wc_MlDsaKey_Free(&p->sk.mldsa.key); #endif } +#ifndef WOLFSSH_NO_MLDSA + else if (p->useMlDsaComposite) { + CompositeParams params; + wc_MlDsaKey_Free(&p->sk.mldsa_composite.base.mldsa); + if (WS_GetCompositeParams(p->pubKeyId, ¶ms) == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + if (ops != NULL) { + ops->free(&p->sk.mldsa_composite.base.trad); + } + } + } +#endif p->keyAllocated = 0; } } @@ -6890,6 +7190,20 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) } #endif /* WOLFSSH_NO_MLDSA */ } +#ifndef WOLFSSH_NO_MLDSA + else if (sigKeyBlock_ptr->useMlDsaComposite) { + ret = VerifyMlDsaComposite(sigKeyBlock_ptr->pubKeyId, + ssh->ctx->heap, + &sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa, + &sigKeyBlock_ptr->sk.mldsa_composite.base.trad, + sig, sigSz, ssh->h, ssh->hSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, + "DoKexDhReply: ML-DSA Composite Signature " + "Verify fail (%d)", ret); + } + } +#endif else { ret = WS_INVALID_ALGO_ID; } @@ -8370,7 +8684,6 @@ static int DoUserAuthRequestRsaCert(WOLFSSH* ssh, WS_UserAuthData_PublicKey* pk, #ifndef WOLFSSH_NO_ECDSA -#define ECDSA_ASN_SIG_SZ 256 /* Utility for DoUserAuthRequestPublicKey() */ /* returns negative for error, positive is size of digest. */ @@ -9449,6 +9762,16 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData, ret = DoUserAuthRequestMlDsa(ssh, &authData->sf.publicKey, authData, (byte)mlLevel, 1, pubKeyBlobSz); } + else if (pkTypeId == ID_MLDSA44_ES256 || + pkTypeId == ID_MLDSA65_ES256 || + pkTypeId == ID_MLDSA87_ES384 || + pkTypeId == ID_MLDSA44_ED25519 || + pkTypeId == ID_MLDSA65_ED25519 || + pkTypeId == ID_MLDSA87_ED448) { + ret = DoUserAuthRequestMlDsaComposite(ssh, + &authData->sf.publicKey, + authData, pkTypeId, pubKeyBlobSz); + } #endif else { wc_HashAlg hash; @@ -12775,6 +13098,20 @@ struct wolfSSH_sigKeyBlockFull { #endif 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 + word32 qSz; + } mldsa_composite; #endif } sk; }; @@ -12782,7 +13119,8 @@ struct wolfSSH_sigKeyBlockFull { #ifdef WOLFSSH_NO_MLDSA #define KEX_SIG_SIZE (512) #else - #define KEX_SIG_SIZE MLDSA_MAX_SIG_SIZE + /* covers the largest trad signature appended by SignHMlDsaComposite() */ + #define KEX_SIG_SIZE (MLDSA_MAX_SIG_SIZE + COMPOSITE_MAX_TRAD_SIG_SZ) #endif #ifdef WOLFSSH_CERTS @@ -13425,6 +13763,168 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, break; } #endif /* WOLFSSH_NO_MLDSA */ +#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: + case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: + { + CompositeParams params; + ret = WS_GetCompositeParams(sigKeyBlock_ptr->pubKeyId, ¶ms); + if (ret == WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "Using Composite Host key"); + + sigKeyBlock_ptr->sk.mldsa_composite.qSz = + sizeof(sigKeyBlock_ptr->sk.mldsa_composite.q); + + /* privateKey[keyIdx].key is the raw envelope, not just + * key data; must go through GetOpenSshKey() to walk it. */ + { +#ifdef WOLFSSH_SMALL_STACK + WS_KeySignature* keySig = (WS_KeySignature*)WMALLOC( + sizeof(WS_KeySignature), heap, DYNTYPE_SSHD); + if (keySig == NULL) { + ret = WS_MEMORY_E; + } + else +#else + WS_KeySignature keySigAlloc; + WS_KeySignature* keySig = &keySigAlloc; +#endif + { + word32 idx = 0; + + XMEMSET(keySig, 0, sizeof(*keySig)); + keySig->keyId = ID_NONE; + keySig->heap = heap; + + ret = GetOpenSshKey(keySig, + ssh->ctx->privateKey[keyIdx].key, + ssh->ctx->privateKey[keyIdx].keySz, &idx); + if (ret == WS_SUCCESS && + keySig->keyId != sigKeyBlock_ptr->pubKeyId) { + wolfSSH_KEY_clean(keySig); + ret = WS_KEY_FORMAT_E; + } + if (ret == WS_SUCCESS) { + /* ecc_key is self-referential under ALT_ECC_SIZE, so + * rebuild via export/import; others copy flat safely. */ + sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa = + keySig->ks.mldsa_composite.mldsa; + wc_MlDsaKey_Free(&keySig->ks.mldsa_composite.mldsa); + { + const CompositeTradOps* tradOps = + WS_GetTradOps(params.tradType); + byte priv[COMPOSITE_MAX_TRAD_PRIV_SZ]; + byte pub[COMPOSITE_MAX_TRAD_PUB_SZ]; + word32 privSz = params.tradPrivSz; + word32 pubSz = params.tradPubSz; + + if (tradOps == NULL || tradOps->exportPrivOnly == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = tradOps->exportPrivOnly( + &keySig->ks.mldsa_composite.trad, + priv, &privSz); + if (ret == 0) { + ret = tradOps->exportPub( + &keySig->ks.mldsa_composite.trad, + pub, &pubSz); + } + if (ret == 0) { + ret = tradOps->init(&sigKeyBlock_ptr-> + sk.mldsa_composite.base.trad, + heap); + if (ret == 0) { + sigKeyBlock_ptr->sk.mldsa_composite.tradInit = 1; + } + } + if (ret == 0) { + ret = tradOps->importPriv( + &sigKeyBlock_ptr-> + sk.mldsa_composite.base.trad, + priv, privSz, pub, pubSz); + } + tradOps->free(&keySig->ks.mldsa_composite.trad); + wc_ForceZero(priv, sizeof(priv)); + /* caller's cleanup frees base.mldsa/.trad on + * failure too; don't free here. */ + if (ret != 0) { + ret = WS_CRYPTO_FAILED; + } + } + } + } +#ifdef WOLFSSH_SMALL_STACK + WFREE(keySig, heap, DYNTYPE_SSHD); +#endif + } + } + + if (ret == 0) { + word32 mldsaPubSz = params.mldsaPubSz; + ret = wc_MlDsaKey_ExportPubRaw( + &sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa, + sigKeyBlock_ptr->sk.mldsa_composite.q, + &mldsaPubSz); + if (ret == 0) { + const CompositeTradOps* ops = WS_GetTradOps( + params.tradType); + word32 eccPubSz = params.tradPubSz; + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->exportPub( + &sigKeyBlock_ptr-> + sk.mldsa_composite.base.trad, + sigKeyBlock_ptr->sk.mldsa_composite.q + + params.mldsaPubSz, + &eccPubSz); + } + } + if (ret == 0) { + sigKeyBlock_ptr->sk.mldsa_composite.qSz = + params.mldsaPubSz + params.tradPubSz; + } + } + + if (!isCert) { + if (ret == 0) { + sigKeyBlock_ptr->sz = (LENGTH_SZ * 2) + + sigKeyBlock_ptr->pubKeyFmtNameSz + + sigKeyBlock_ptr->sk.mldsa_composite.qSz; + c32toa(sigKeyBlock_ptr->sz, scratchLen); + ret = wc_HashUpdate(hash, hashId, + scratchLen, LENGTH_SZ); + } + if (ret == 0) { + c32toa(sigKeyBlock_ptr->pubKeyFmtNameSz, scratchLen); + ret = wc_HashUpdate(hash, hashId, + scratchLen, LENGTH_SZ); + } + if (ret == 0) + ret = wc_HashUpdate(hash, hashId, + (byte*)sigKeyBlock_ptr->pubKeyFmtName, + sigKeyBlock_ptr->pubKeyFmtNameSz); + if (ret == 0) { + c32toa(sigKeyBlock_ptr->sk.mldsa_composite.qSz, + scratchLen); + ret = wc_HashUpdate(hash, hashId, + scratchLen, LENGTH_SZ); + } + if (ret == 0) + ret = wc_HashUpdate(hash, hashId, + sigKeyBlock_ptr->sk.mldsa_composite.q, + sigKeyBlock_ptr->sk.mldsa_composite.qSz); + } + } + break; + } +#endif default: ret = WS_INVALID_ALGO_ID; @@ -14575,6 +15075,14 @@ static int SignH(WOLFSSH* ssh, byte* sig, word32* sigSz, case ID_X509V3_MLDSA87: ret = SignHMlDsa(ssh, sig, sigSz, sigKey); break; + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: + case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: + ret = SignHMlDsaComposite(ssh, sig, sigSz, sigKey); + break; #endif default: ret = WS_INVALID_ALGO_ID; @@ -14907,6 +15415,23 @@ int SendKexDhReply(WOLFSSH* ssh) ) { wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa.key); } + else if (sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA44_ES256 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA65_ES256 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA87_ES384 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA44_ED25519 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA65_ED25519 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA87_ED448) { + CompositeParams params; + wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa); + if (sigKeyBlock_ptr->sk.mldsa_composite.tradInit && + WS_GetCompositeParams(sigKeyBlock_ptr->pubKeyFmtId, ¶ms) + == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + if (ops != NULL) { + ops->free(&sigKeyBlock_ptr->sk.mldsa_composite.base.trad); + } + } + } #endif } @@ -15046,6 +15571,22 @@ int SendKexDhReply(WOLFSSH* ssh) idx += sigKeyBlock_ptr->sk.mldsa.qSz; } break; + + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: + case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: + { + /* composite key block: ML-DSA pubkey followed by trad pubkey */ + c32toa(sigKeyBlock_ptr->sk.mldsa_composite.qSz, output + idx); + idx += LENGTH_SZ; + WMEMCPY(output + idx, sigKeyBlock_ptr->sk.mldsa_composite.q, + sigKeyBlock_ptr->sk.mldsa_composite.qSz); + idx += sigKeyBlock_ptr->sk.mldsa_composite.qSz; + } + break; #endif #ifdef WOLFSSH_CERTS @@ -17719,7 +18260,7 @@ static int BuildUserAuthRequestMlDsa(WOLFSSH* ssh, return ret; } - sigSz = (word32)keySig->sigSz; + sigSz = keySig->sigSz; sig = (byte*)WMALLOC(sigSz, keySig->heap, DYNTYPE_BUFFER); if (sig == NULL) @@ -17936,6 +18477,17 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, break; #endif #endif +#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: + case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: + ret = PrepareUserAuthRequestMlDsaComposite(ssh, + payloadSz, authData, keySig); + break; +#endif default: ret = WS_INVALID_ALGO_ID; } @@ -18109,6 +18661,26 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, break; #endif #endif +#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: + case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: + c32toa(pk->publicKeyTypeSz, output + begin); + begin += LENGTH_SZ; + WMEMCPY(output + begin, + pk->publicKeyType, pk->publicKeyTypeSz); + begin += pk->publicKeyTypeSz; + c32toa(pk->publicKeySz, output + begin); + begin += LENGTH_SZ; + WMEMCPY(output + begin, pk->publicKey, pk->publicKeySz); + begin += pk->publicKeySz; + ret = BuildUserAuthRequestMlDsaComposite(ssh, output, + &begin, authData, sigStart, sigStartIdx, keySig); + break; +#endif default: ret = WS_INVALID_ALGO_ID; } @@ -20395,7 +20967,1367 @@ void AddAssign64(word32* addend1, word32 addend2) #endif /* WOLFSSH_SFTP */ -#ifdef WOLFSSH_TEST_INTERNAL + + +#ifndef WOLFSSH_NO_MLDSA + +int WS_GetCompositeParams(byte keyId, CompositeParams* params) +{ + XMEMSET(params, 0, sizeof(*params)); + params->keyId = keyId; + + switch (keyId) { +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + case ID_MLDSA44_ES256: + params->mldsaLevel = WC_ML_DSA_44; + params->mldsaSigSz = WC_MLDSA_44_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_44_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ECC; + params->tradHashId = WC_HASH_TYPE_SHA256; + params->tradHashSz = WC_SHA256_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA44-ECDSA-P256-SHA256"; + params->labelSz = (word32)XSTRLEN(params->label); + /* uncompressed point: 1 (type octet) + 2 * coordinate */ + params->tradPubSz = 1 + (2 * ECC_P256_COORD_SZ); + /* worst case: 2 * (LENGTH_SZ + P256 coordinate + sign pad) */ + params->tradSigSz = 2 * (LENGTH_SZ + ECC_P256_COORD_SZ + 1); + params->tradPrivSz = ECC_P256_COORD_SZ; + params->eccCurveId = ECC_SECP256R1; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + case ID_MLDSA65_ES256: + params->mldsaLevel = WC_ML_DSA_65; + params->mldsaSigSz = WC_MLDSA_65_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_65_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ECC; + params->tradHashId = WC_HASH_TYPE_SHA512; + params->tradHashSz = WC_SHA512_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA65-ECDSA-P256-SHA512"; + params->labelSz = (word32)XSTRLEN(params->label); + /* uncompressed point: 1 (type octet) + 2 * coordinate */ + params->tradPubSz = 1 + (2 * ECC_P256_COORD_SZ); + /* worst case: 2 * (LENGTH_SZ + P256 coordinate + sign pad) */ + params->tradSigSz = 2 * (LENGTH_SZ + ECC_P256_COORD_SZ + 1); + params->tradPrivSz = ECC_P256_COORD_SZ; + params->eccCurveId = ECC_SECP256R1; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + case ID_MLDSA87_ES384: + params->mldsaLevel = WC_ML_DSA_87; + params->mldsaSigSz = WC_MLDSA_87_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_87_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ECC; + params->tradHashId = WC_HASH_TYPE_SHA512; + params->tradHashSz = WC_SHA512_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA87-ECDSA-P384-SHA512"; + params->labelSz = (word32)XSTRLEN(params->label); + /* uncompressed point: 1 (type octet) + 2 * coordinate */ + params->tradPubSz = 1 + (2 * ECC_P384_COORD_SZ); + /* worst case: 2 * (LENGTH_SZ + P384 coordinate + sign pad) */ + params->tradSigSz = 2 * (LENGTH_SZ + ECC_P384_COORD_SZ + 1); + params->tradPrivSz = ECC_P384_COORD_SZ; + params->eccCurveId = ECC_SECP384R1; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + case ID_MLDSA44_ED25519: + params->mldsaLevel = WC_ML_DSA_44; + params->mldsaSigSz = WC_MLDSA_44_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_44_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ED25519; + params->tradHashId = WC_HASH_TYPE_SHA512; + params->tradHashSz = WC_SHA512_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA44-Ed25519-SHA512"; + params->labelSz = (word32)XSTRLEN(params->label); + params->tradPubSz = ED25519_PUB_KEY_SIZE; + params->tradSigSz = ED25519_SIG_SIZE; + params->tradPrivSz = ED25519_KEY_SIZE; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + case ID_MLDSA65_ED25519: + params->mldsaLevel = WC_ML_DSA_65; + params->mldsaSigSz = WC_MLDSA_65_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_65_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ED25519; + params->tradHashId = WC_HASH_TYPE_SHA512; + params->tradHashSz = WC_SHA512_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA65-Ed25519-SHA512"; + params->labelSz = (word32)XSTRLEN(params->label); + params->tradPubSz = ED25519_PUB_KEY_SIZE; + params->tradSigSz = ED25519_SIG_SIZE; + params->tradPrivSz = ED25519_KEY_SIZE; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + case ID_MLDSA87_ED448: + params->mldsaLevel = WC_ML_DSA_87; + params->mldsaSigSz = WC_MLDSA_87_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_87_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ED448; + params->tradHashId = WC_HASH_TYPE_SHAKE256; + /* SHAKE256 truncated to a fixed 64-byte digest for this combo */ + params->tradHashSz = 64; + params->label = "COMPSIG-MLDSA87-Ed448-SHAKE256"; + params->labelSz = (word32)XSTRLEN(params->label); + params->tradPubSz = ED448_PUB_KEY_SIZE; + params->tradSigSz = ED448_SIG_SIZE; + params->tradPrivSz = ED448_KEY_SIZE; + break; +#endif + default: + return WS_BAD_ARGUMENT; + } + + /* guards mPrime buffer sizing in VerifyMlDsaComposite/ + * SignHMlDsaComposite; fail loudly instead of overflowing */ + if (params->labelSz > COMPOSITE_MAX_LABEL_SZ) { + WLOG(WS_LOG_ERROR, "Composite label size %u exceeds " + "COMPOSITE_MAX_LABEL_SZ %u", params->labelSz, + (word32)COMPOSITE_MAX_LABEL_SZ); + return WS_BUFFER_E; + } + + return WS_SUCCESS; +} + +int WS_Hash_Helper(enum wc_HashType hashId, const byte* msg, word32 msgSz, + byte* hash, word32 hashSz) +{ + int ret; +#ifdef WOLFSSL_SHAKE256 + if (hashId == WC_HASH_TYPE_SHAKE256) { + wc_Shake shake; + ret = wc_InitShake256(&shake, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_Shake256_Update(&shake, msg, msgSz); + if (ret == 0) { + ret = wc_Shake256_Final(&shake, hash, hashSz); + } + wc_Shake256_Free(&shake); + } + return ret; + } +#endif + return wc_Hash(hashId, msg, msgSz, hash, hashSz); +} + +/* one CompositeTradOps instance per trad algorithm; see wolfssh/internal.h */ + +#ifndef WOLFSSH_NO_ECDSA +/* returns 0 on success, negative on failure (wc_ecc_init_ex() code) */ +static int CompositeEccInit(void* key, void* heap) +{ + return wc_ecc_init_ex((ecc_key*)key, heap, INVALID_DEVID); +} + +/* no return value */ +static void CompositeEccFree(void* key) +{ + wc_ecc_free((ecc_key*)key); +} + +/* returns 0 on success, negative on failure (wc_ecc_import_x963() code) */ +static int CompositeEccImportPub(void* key, const byte* pub, word32 pubSz) +{ + return wc_ecc_import_x963(pub, pubSz, (ecc_key*)key); +} + +/* returns 0 on success, negative wc_ecc_import_private_key() code on + * failure */ +static int CompositeEccImportPriv(void* key, const byte* priv, word32 privSz, + const byte* pub, word32 pubSz) +{ + return wc_ecc_import_private_key(priv, privSz, pub, pubSz, (ecc_key*)key); +} + +/* returns 0 on success, negative on failure (wc_ecc_export_private_only() code) */ +static int CompositeEccExportPrivOnly(void* key, byte* out, word32* outSz) +{ + return wc_ecc_export_private_only((ecc_key*)key, out, outSz); +} + +/* returns 0 on success, negative on failure (wc_ecc_export_x963() code) */ +static int CompositeEccExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ecc_export_x963((ecc_key*)key, out, outSz); +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEccSign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* mPrime, word32 mPrimeLen, + byte* wireSig, word32* wireSigSz) +{ + int ret; + word32 asnSigSz = ECDSA_ASN_SIG_SZ; + byte digest[WC_MAX_DIGEST_SIZE]; +#ifdef WOLFSSH_SMALL_STACK + byte* asnSig = NULL; +#else + byte asnSig[ECDSA_ASN_SIG_SZ]; +#endif + + if (tradHashSz > WC_MAX_DIGEST_SIZE) + return WS_BUFFER_E; + +#ifdef WOLFSSH_SMALL_STACK + asnSig = (byte*)WMALLOC(ECDSA_ASN_SIG_SZ, heap, DYNTYPE_TEMP); + if (asnSig == NULL) + return WS_MEMORY_E; +#else + (void)heap; +#endif + + ret = WS_Hash_Helper(tradHashId, mPrime, mPrimeLen, digest, tradHashSz); + if (ret == 0) { + ret = wc_ecc_sign_hash(digest, tradHashSz, asnSig, &asnSigSz, + rng, (ecc_key*)key); + } + if (ret == 0) { + word32 rSz = MAX_ECC_BYTES + ECC_MAX_PAD_SZ, + sSz = MAX_ECC_BYTES + ECC_MAX_PAD_SZ; +#ifdef WOLFSSH_SMALL_STACK + byte* rBuf = NULL; + byte* sBuf = NULL; + + rBuf = (byte*)WMALLOC(MAX_ECC_BYTES + ECC_MAX_PAD_SZ, heap, + DYNTYPE_TEMP); + if (rBuf == NULL) + ret = WS_MEMORY_E; + if (ret == 0) { + sBuf = (byte*)WMALLOC(MAX_ECC_BYTES + ECC_MAX_PAD_SZ, heap, + DYNTYPE_TEMP); + if (sBuf == NULL) + ret = WS_MEMORY_E; + } +#else + byte rBuf[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; + byte sBuf[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; +#endif + + if (ret == 0) { + ret = wc_ecc_sig_to_rs(asnSig, asnSigSz, rBuf, &rSz, sBuf, &sSz); + } + if (ret == 0) { + word32 offset = 0; + byte rPad = (rBuf[0] & 0x80) ? 1 : 0; + byte sPad = (sBuf[0] & 0x80) ? 1 : 0; + + /* 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) { + 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; + } + } +#ifdef WOLFSSH_SMALL_STACK + if (rBuf != NULL) { + WFREE(rBuf, heap, DYNTYPE_TEMP); + } + if (sBuf != NULL) { + WFREE(sBuf, heap, DYNTYPE_TEMP); + } +#endif + } + if (ret != 0 && ret != WS_BAD_ARGUMENT && ret != WS_MEMORY_E) { + ret = WS_ECC_E; + } + +#ifdef WOLFSSH_SMALL_STACK + if (asnSig != NULL) { + WFREE(asnSig, heap, DYNTYPE_TEMP); + } +#endif + + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEccVerify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* mPrime, word32 mPrimeLen) +{ + int ret; + const byte* r = NULL; + const byte* s = NULL; + word32 rSz = 0, sSz = 0; + word32 i = 0; + word32 asnSigSz = ECDSA_ASN_SIG_SZ; +#ifdef WOLFSSH_SMALL_STACK + byte* asnSig = NULL; +#else + byte asnSig[ECDSA_ASN_SIG_SZ]; +#endif + + if (tradHashSz > WC_MAX_DIGEST_SIZE) + return WS_BUFFER_E; + +#ifdef WOLFSSH_SMALL_STACK + asnSig = (byte*)WMALLOC(ECDSA_ASN_SIG_SZ, heap, DYNTYPE_TEMP); + if (asnSig == NULL) + return WS_MEMORY_E; +#else + (void)heap; +#endif + + ret = GetStringRef(&rSz, &r, wireSig, wireSigSz, &i); + if (ret == WS_SUCCESS) { + ret = GetStringRef(&sSz, &s, wireSig, wireSigSz, &i); + } + /* GetStringRef() only bounds-checks each string; it doesn't require + * reaching the end, so reject any trailing bytes after r/s here. */ + if (ret == WS_SUCCESS && i != wireSigSz) { + ret = WS_KEY_FORMAT_E; + } + if (ret == WS_SUCCESS) { + ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz, asnSig, &asnSigSz); + if (ret != 0) ret = WS_ECC_E; + } + if (ret == WS_SUCCESS) { + byte digest[WC_MAX_DIGEST_SIZE]; + ret = WS_Hash_Helper(tradHashId, mPrime, mPrimeLen, digest, tradHashSz); + if (ret == 0) { + ret = wc_SignatureVerifyHash( + tradHashId, + WC_SIGNATURE_TYPE_ECC, + digest, tradHashSz, + asnSig, asnSigSz, + (ecc_key*)key, + sizeof(ecc_key)); + } + if (ret != 0) { + ret = WS_ECC_E; + } + } + +#ifdef WOLFSSH_SMALL_STACK + if (asnSig != NULL) { + WFREE(asnSig, heap, DYNTYPE_TEMP); + } +#endif + + return ret; +} + +static const CompositeTradOps compositeEccOps = { + CompositeEccInit, CompositeEccFree, + CompositeEccImportPub, CompositeEccImportPriv, + CompositeEccExportPrivOnly, CompositeEccExportPub, + CompositeEccSign, CompositeEccVerify, + TRAD_TYPE_ECC +}; +#endif /* !WOLFSSH_NO_ECDSA */ + +#ifndef WOLFSSH_NO_ED25519 +/* returns 0 on success, negative on failure (wc_ed25519_init_ex() code) */ +static int CompositeEd25519Init(void* key, void* heap) +{ + return wc_ed25519_init_ex((ed25519_key*)key, heap, INVALID_DEVID); +} + +/* no return value */ +static void CompositeEd25519Free(void* key) +{ + wc_ed25519_free((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) +{ + return wc_ed25519_import_public(pub, pubSz, (ed25519_key*)key); +} + +/* returns 0 on success, negative wc_ed25519_import_private_key() code on + * failure */ +static int CompositeEd25519ImportPriv(void* key, const byte* priv, + word32 privSz, const byte* pub, word32 pubSz) +{ + return wc_ed25519_import_private_key(priv, privSz, pub, pubSz, + (ed25519_key*)key); +} + +/* returns 0 on success, negative wc_ed25519_export_private_only() code on + * failure */ +static int CompositeEd25519ExportPrivOnly(void* key, byte* out, word32* outSz) +{ + return wc_ed25519_export_private_only((ed25519_key*)key, out, outSz); +} + +/* returns 0 on success, negative wc_ed25519_export_public() code on + * failure */ +static int CompositeEd25519ExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ed25519_export_public((ed25519_key*)key, out, outSz); +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEd25519Sign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* mPrime, word32 mPrimeLen, + byte* wireSig, word32* wireSigSz) +{ + int ret; + word32 sigSz = ED25519_SIG_SIZE; + + (void)rng; + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + if (*wireSigSz < ED25519_SIG_SIZE) { + return WS_BAD_ARGUMENT; + } + + ret = wc_ed25519_sign_msg(mPrime, mPrimeLen, wireSig, &sigSz, + (ed25519_key*)key); + if (ret != 0 || sigSz != ED25519_SIG_SIZE) { + ret = WS_ED25519_E; + } + else { + *wireSigSz = sigSz; + } + + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEd25519Verify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* mPrime, word32 mPrimeLen) +{ + int ret; + int res = 0; + + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + ret = wc_ed25519_verify_msg(wireSig, wireSigSz, mPrime, mPrimeLen, + &res, (ed25519_key*)key); + if (ret != 0 || res != 1) { + ret = WS_ED25519_E; + } + + return ret; +} + +static const CompositeTradOps compositeEd25519Ops = { + CompositeEd25519Init, CompositeEd25519Free, + CompositeEd25519ImportPub, CompositeEd25519ImportPriv, + CompositeEd25519ExportPrivOnly, CompositeEd25519ExportPub, + CompositeEd25519Sign, CompositeEd25519Verify, + TRAD_TYPE_ED25519 +}; +#endif /* !WOLFSSH_NO_ED25519 */ + +#ifdef HAVE_ED448 +/* returns 0 on success, negative wc_ed448_init_ex() code on failure */ +static int CompositeEd448Init(void* key, void* heap) +{ + return wc_ed448_init_ex((ed448_key*)key, heap, INVALID_DEVID); +} + +/* no return value */ +static void CompositeEd448Free(void* key) +{ + wc_ed448_free((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) +{ + return wc_ed448_import_public(pub, pubSz, (ed448_key*)key); +} + +/* returns 0 on success, negative wc_ed448_import_private_key() code on + * failure */ +static int CompositeEd448ImportPriv(void* key, const byte* priv, + word32 privSz, const byte* pub, word32 pubSz) +{ + return wc_ed448_import_private_key(priv, privSz, pub, pubSz, + (ed448_key*)key); +} + +/* returns 0 on success, negative wc_ed448_export_private_only() code on failure */ +static int CompositeEd448ExportPrivOnly(void* key, byte* out, word32* outSz) +{ + return wc_ed448_export_private_only((ed448_key*)key, out, outSz); +} + +/* returns 0 on success, negative wc_ed448_export_public() code on failure */ +static int CompositeEd448ExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ed448_export_public((ed448_key*)key, out, outSz); +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEd448Sign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* mPrime, word32 mPrimeLen, + byte* wireSig, word32* wireSigSz) +{ + int ret; + word32 sigSz = ED448_SIG_SIZE; + + (void)rng; + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + if (*wireSigSz < ED448_SIG_SIZE) { + return WS_BAD_ARGUMENT; + } + + ret = wc_ed448_sign_msg(mPrime, mPrimeLen, wireSig, &sigSz, + (ed448_key*)key, NULL, 0); + if (ret != 0 || sigSz != ED448_SIG_SIZE) { + ret = WS_ED448_E; + } + else { + *wireSigSz = sigSz; + } + + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEd448Verify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* mPrime, word32 mPrimeLen) +{ + int ret; + int res = 0; + + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + ret = wc_ed448_verify_msg(wireSig, wireSigSz, mPrime, mPrimeLen, + &res, (ed448_key*)key, NULL, 0); + if (ret != 0 || res != 1) { + ret = WS_ED448_E; + } + + return ret; +} + +static const CompositeTradOps compositeEd448Ops = { + CompositeEd448Init, CompositeEd448Free, + CompositeEd448ImportPub, CompositeEd448ImportPriv, + CompositeEd448ExportPrivOnly, CompositeEd448ExportPub, + CompositeEd448Sign, CompositeEd448Verify, + TRAD_TYPE_ED448 +}; +#endif /* HAVE_ED448 */ + +/* returns matching CompositeTradOps for tradType, NULL if unsupported */ +const CompositeTradOps* WS_GetTradOps(byte tradType) +{ + switch (tradType) { +#ifndef WOLFSSH_NO_ECDSA + case TRAD_TYPE_ECC: + return &compositeEccOps; +#endif +#ifndef WOLFSSH_NO_ED25519 + case TRAD_TYPE_ED25519: + return &compositeEd25519Ops; +#endif +#ifdef HAVE_ED448 + case TRAD_TYPE_ED448: + return &compositeEd448Ops; +#endif + default: + return NULL; + } +} + +/* mPrime scratch buffer size for VerifyMlDsaComposite/SignHMlDsaComposite */ +#define COMPOSITE_M_PRIME_SZ \ + (COMPOSITE_DOMAIN_PREFIX_SZ + COMPOSITE_MAX_LABEL_SZ + 1 + \ + WC_MAX_DIGEST_SIZE) + +/* Assembles mPrime = PREFIX||label||0x00||hash; single source of truth + * for the domain-separator layout so sign/verify can't drift apart. */ +static void BuildCompositeMPrime(const CompositeParams* params, + const byte* hash, byte* mPrime) +{ + XMEMCPY(mPrime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ, params->label, + params->labelSz); + mPrime[COMPOSITE_DOMAIN_PREFIX_SZ + params->labelSz] = 0; + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ + params->labelSz + 1, hash, + params->tradHashSz); +} + +/* returns WS_SUCCESS if sig verifies, negative WS_* error code otherwise */ +static int VerifyMlDsaComposite(byte keyId, void* heap, + MlDsaKey* mldsa, void* tradKey, + const byte* sig, word32 sigSz, + const byte* msg, word32 msgSz) +{ + int ret = WS_SUCCESS; + CompositeParams params; + int status = 0; + word32 mPrimeLen = 0; +#ifdef WOLFSSH_SMALL_STACK + byte* hash = NULL; + byte* mPrime = NULL; +#else + byte hash[WC_MAX_DIGEST_SIZE]; + byte mPrime[COMPOSITE_M_PRIME_SZ]; +#endif + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + if (params.tradHashSz > WC_MAX_DIGEST_SIZE) { + return WS_BUFFER_E; + } + + /* prevents "sigSz - mldsaSigSz" underflow below (sig/sigSz are + * wire-controlled) */ + if (sigSz < params.mldsaSigSz) { + return WS_KEY_FORMAT_E; + } + + /* ED25519/ED448 are fixed size and must match exactly; ECC's r/s + * are variable, so bound the trad region too against oversized claims. */ + if (params.tradType == TRAD_TYPE_ED25519 || + params.tradType == TRAD_TYPE_ED448) { + if (sigSz != (params.mldsaSigSz + params.tradSigSz)) { + return WS_KEY_FORMAT_E; + } + } + else if (sigSz - params.mldsaSigSz > params.tradSigSz) { + return WS_KEY_FORMAT_E; + } + +#ifdef WOLFSSH_SMALL_STACK + hash = (byte*)WMALLOC(WC_MAX_DIGEST_SIZE, heap, DYNTYPE_TEMP); + if (hash == NULL) + ret = WS_MEMORY_E; + if (ret == WS_SUCCESS) { + mPrime = (byte*)WMALLOC(COMPOSITE_M_PRIME_SZ, heap, DYNTYPE_TEMP); + if (mPrime == NULL) + ret = WS_MEMORY_E; + } +#endif + + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + + params.tradHashSz; + + if (ret == WS_SUCCESS) { + ret = WS_Hash_Helper(params.tradHashId, msg, msgSz, hash, + params.tradHashSz); + if (ret != 0) ret = WS_CRYPTO_FAILED; + } + + if (ret == WS_SUCCESS) { + BuildCompositeMPrime(¶ms, hash, mPrime); + } + + /* cheap trad verify first, so garbage sigs are rejected cheaply + * pre-auth */ + if (ret == WS_SUCCESS) { + const byte* tradSig = sig + params.mldsaSigSz; + word32 tradSigSz = sigSz - params.mldsaSigSz; + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->verify(tradKey, heap, + params.tradHashId, params.tradHashSz, + tradSig, tradSigSz, mPrime, mPrimeLen); + } + } + + if (ret == WS_SUCCESS) { + ret = wc_MlDsaKey_VerifyCtx(mldsa, + sig, params.mldsaSigSz, + (const byte*)params.label, params.labelSz, + mPrime, mPrimeLen, + &status); + if (ret != 0 || status != 1) { + WLOG(WS_LOG_DEBUG, + "VerifyMlDsaComposite: ML-DSA Verify fail (%d, status=%d)", + ret, status); + ret = WS_MLDSA_E; + } + } + +#ifdef WOLFSSH_SMALL_STACK + if (hash != NULL) { + wc_ForceZero(hash, WC_MAX_DIGEST_SIZE); + WFREE(hash, heap, DYNTYPE_TEMP); + } + if (mPrime != NULL) { + wc_ForceZero(mPrime, COMPOSITE_M_PRIME_SZ); + WFREE(mPrime, heap, DYNTYPE_TEMP); + } +#else + wc_ForceZero(hash, WC_MAX_DIGEST_SIZE); + wc_ForceZero(mPrime, COMPOSITE_M_PRIME_SZ); +#endif + + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int ParseMlDsaCompositePubKey(WOLFSSH* ssh, + struct wolfSSH_sigKeyBlock* sigKeyBlock_ptr, + byte* pubKey, word32 pubKeySz, byte keyId) +{ + int ret; + int mldsaInit = 0; + int tradInit = 0; + const byte* pub; + word32 pubSz, pubKeyIdx = 0; + CompositeParams params; + const CompositeTradOps* ops; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + ops = WS_GetTradOps(params.tradType); + + ret = InitCompositeKeyPair(¶ms, + &sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa, + &sigKeyBlock_ptr->sk.mldsa_composite.base.trad, ops, + ssh->ctx->heap, &mldsaInit, &tradInit); + + if (ret == WS_SUCCESS) { + ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx); + } + if (ret == WS_SUCCESS) + ret = GetStringRef(&pubSz, &pub, pubKey, pubKeySz, &pubKeyIdx); + if (ret == WS_SUCCESS) { + if (pubSz != (params.mldsaPubSz + params.tradPubSz)) { + ret = WS_KEY_FORMAT_E; + } + } + if (ret == WS_SUCCESS) + ret = wc_MlDsaKey_ImportPubRaw( + &sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa, + pub, params.mldsaPubSz); + if (ret == WS_SUCCESS) { + ret = ops->importPub(&sigKeyBlock_ptr->sk.mldsa_composite.base.trad, + pub + params.mldsaPubSz, params.tradPubSz); + } + + if (ret == WS_SUCCESS) { + sigKeyBlock_ptr->keyAllocated = 1; + } + else { + if (mldsaInit) { + wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa); + } + if (tradInit) { + ops->free(&sigKeyBlock_ptr->sk.mldsa_composite.base.trad); + } + /* preserve diagnosable reasons; collapse raw wolfCrypt codes from + * ImportPubRaw/importPub to keep a stable public error surface */ + if (ret != WS_UNIMPLEMENTED_E && ret != WS_MEMORY_E && + ret != WS_KEY_FORMAT_E) { + ret = WS_INVALID_ALGO_ID; + } + } + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int SignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, + struct wolfSSH_sigKeyBlockFull *sigKey) +{ + int ret; + CompositeParams params; + word32 mPrimeLen = 0; + word32 mldsaSigSz; + byte keyId = sigKey->pubKeyId; +#ifdef WOLFSSH_SMALL_STACK + byte* hash = NULL; + byte* mPrime = NULL; +#else + byte hash[WC_MAX_DIGEST_SIZE]; + byte mPrime[COMPOSITE_M_PRIME_SZ]; +#endif + + WLOG(WS_LOG_DEBUG, "Entering SignHMlDsaComposite()"); + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + if (params.tradHashSz > WC_MAX_DIGEST_SIZE) { + return WS_BUFFER_E; + } + + mldsaSigSz = params.mldsaSigSz; + + /* verify sig buffer fits worst case before trad sign appends to it */ + if (*sigSz < (params.mldsaSigSz + params.tradSigSz)) { + return WS_BAD_ARGUMENT; + } + +#ifdef WOLFSSH_SMALL_STACK + hash = (byte*)WMALLOC(WC_MAX_DIGEST_SIZE, ssh->ctx->heap, DYNTYPE_TEMP); + if (hash == NULL) + ret = WS_MEMORY_E; + if (ret == WS_SUCCESS) { + mPrime = (byte*)WMALLOC(COMPOSITE_M_PRIME_SZ, ssh->ctx->heap, + DYNTYPE_TEMP); + if (mPrime == NULL) + ret = WS_MEMORY_E; + } +#endif + + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + + params.tradHashSz; + + if (ret == WS_SUCCESS) { + ret = WS_Hash_Helper(params.tradHashId, ssh->h, ssh->hSz, hash, + params.tradHashSz); + if (ret != 0) ret = WS_CRYPTO_FAILED; + } + + if (ret == WS_SUCCESS) { + BuildCompositeMPrime(¶ms, hash, mPrime); + } + + if (ret == WS_SUCCESS) { + ret = wc_MlDsaKey_SignCtx(&sigKey->sk.mldsa_composite.base.mldsa, + (const byte*)params.label, + params.labelSz, + sig, &mldsaSigSz, mPrime, mPrimeLen, + ssh->rng); + if (ret != 0 || mldsaSigSz != params.mldsaSigSz) { + WLOG(WS_LOG_DEBUG, "SignHMlDsaComposite: ML-DSA sign fail (%d)", + ret); + ret = WS_MLDSA_E; + } + } + + if (ret == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + word32 wireSigSz = params.tradSigSz; + ret = ops->sign(&sigKey->sk.mldsa_composite.base.trad, ssh->rng, + ssh->ctx->heap, params.tradHashId, params.tradHashSz, + mPrime, mPrimeLen, sig + params.mldsaSigSz, &wireSigSz); + if (ret == WS_SUCCESS) { + *sigSz = params.mldsaSigSz + wireSigSz; + } + else { + WLOG(WS_LOG_DEBUG, "SignHMlDsaComposite: trad sign fail (%d)", + ret); + } + } + } + +#ifdef WOLFSSH_SMALL_STACK + if (hash != NULL) { + wc_ForceZero(hash, WC_MAX_DIGEST_SIZE); + WFREE(hash, ssh->ctx->heap, DYNTYPE_TEMP); + } + if (mPrime != NULL) { + wc_ForceZero(mPrime, COMPOSITE_M_PRIME_SZ); + WFREE(mPrime, ssh->ctx->heap, DYNTYPE_TEMP); + } +#else + wc_ForceZero(hash, WC_MAX_DIGEST_SIZE); + wc_ForceZero(mPrime, COMPOSITE_M_PRIME_SZ); +#endif + + WLOG(WS_LOG_DEBUG, "Leaving SignHMlDsaComposite(), ret = %d", ret); + return ret; +} + +static int PrepareUserAuthRequestMlDsaComposite(WOLFSSH* ssh, word32* payloadSz, + const WS_UserAuthData* authData, WS_KeySignature* keySig) +{ + int ret = WS_SUCCESS; + CompositeParams params; + byte keyId = ID_NONE; + + WLOG(WS_LOG_DEBUG, "Entering PrepareUserAuthRequestMlDsaComposite()"); + if (ssh == NULL || payloadSz == NULL || authData == NULL || keySig == NULL) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS) { + keyId = keySig->keyId; + ret = WS_GetCompositeParams(keyId, ¶ms); + } + + if (ret == WS_SUCCESS) { + word32 idx = 0; + + /* Composite keys are OpenSSH-format only; keySig is already + * zeroed/keyId-set by the caller, no further pre-init needed. */ + ret = GetOpenSshKey(keySig, + authData->sf.publicKey.privateKey, + authData->sf.publicKey.privateKeySz, &idx); + if (ret == WS_SUCCESS && keySig->keyId != keyId) { + wolfSSH_KEY_clean(keySig); + keySig->keyId = ID_NONE; + ret = WS_KEY_FORMAT_E; + } + } + + if (ret == WS_SUCCESS) { + if (authData->sf.publicKey.hasSignature) { + word32 sigSz = params.mldsaSigSz + params.tradSigSz; + *payloadSz += ( + LENGTH_SZ * 3) + sigSz + authData->sf.publicKey.publicKeyTypeSz; + keySig->sigSz = sigSz; + } + } + + WLOG(WS_LOG_DEBUG, + "Leaving PrepareUserAuthRequestMlDsaComposite(), ret = %d", ret); + return ret; +} + +static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + byte* output, word32* idx, + const WS_UserAuthData* authData, + const byte* sigStart, word32 sigStartIdx, + WS_KeySignature* keySig) +{ + word32 begin; + int ret = WS_SUCCESS; + byte* sig = NULL; + word32 sigSz; + byte* checkData = NULL; + word32 checkDataSz = 0; + byte* hash = NULL; + byte* mPrime = NULL; + word32 mldsaSigSz; + word32 mPrimeLen; + CompositeParams params; + byte keyId; + + WLOG(WS_LOG_DEBUG, "Entering BuildUserAuthRequestMlDsaComposite()"); + if (ssh == NULL || output == NULL || idx == NULL || authData == NULL || + sigStart == NULL || keySig == NULL) { + return WS_BAD_ARGUMENT; + } + keyId = keySig->keyId; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + mldsaSigSz = params.mldsaSigSz; + sigSz = keySig->sigSz; + + /* sigSz is already the worst-case total; slack below isn't load-bearing */ + sig = (byte*)WMALLOC(sigSz + COMPOSITE_SIG_ALLOC_SLACK_SZ, + keySig->heap, DYNTYPE_BUFFER); + if (sig == NULL) + ret = WS_MEMORY_E; + + begin = *idx; + + if (ret == WS_SUCCESS) { + checkDataSz = LENGTH_SZ + ssh->sessionIdSz + (begin - sigStartIdx); + checkData = (byte*)WMALLOC(checkDataSz, keySig->heap, DYNTYPE_TEMP); + if (checkData == NULL) + ret = WS_MEMORY_E; + } + + if (ret == WS_SUCCESS) { + word32 i = 0; + + c32toa(ssh->sessionIdSz, checkData + i); + i += LENGTH_SZ; + WMEMCPY(checkData + i, ssh->sessionId, ssh->sessionIdSz); + i += ssh->sessionIdSz; + WMEMCPY(checkData + i, sigStart, begin - sigStartIdx); + } + + if (ret == WS_SUCCESS) { + hash = (byte*)WMALLOC(params.tradHashSz, keySig->heap, DYNTYPE_TEMP); + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + + 1 + params.tradHashSz; + mPrime = (byte*)WMALLOC(mPrimeLen, keySig->heap, DYNTYPE_TEMP); + + if (hash == NULL || mPrime == NULL) { + ret = WS_MEMORY_E; + } + } + + if (ret == WS_SUCCESS) { + ret = WS_Hash_Helper(params.tradHashId, checkData, checkDataSz, hash, + params.tradHashSz); + if (ret != 0) { + ret = WS_CRYPTO_FAILED; + } + } + + if (ret == WS_SUCCESS) { + BuildCompositeMPrime(¶ms, hash, mPrime); + } + + if (ret == WS_SUCCESS) { + WLOG(WS_LOG_INFO, "Signing with hybrid composite (ML-DSA component)."); + ret = wc_MlDsaKey_SignCtx(&keySig->ks.mldsa_composite.mldsa, + (const byte*)params.label, + params.labelSz, + sig, &mldsaSigSz, mPrime, mPrimeLen, + ssh->rng); + if (ret != 0 || mldsaSigSz != params.mldsaSigSz) { + WLOG(WS_LOG_DEBUG, + "BUARMlDsaComposite: ML-DSA sign fail (%d)", ret); + ret = WS_MLDSA_E; + } + } + + if (ret == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + word32 wireSigSz = params.tradSigSz; + WLOG(WS_LOG_INFO, + "Signing with hybrid composite (trad component)."); + ret = ops->sign(&keySig->ks.mldsa_composite.trad, ssh->rng, + keySig->heap, params.tradHashId, params.tradHashSz, + mPrime, mPrimeLen, sig + params.mldsaSigSz, &wireSigSz); + if (ret == WS_SUCCESS) { + sigSz = params.mldsaSigSz + wireSigSz; + } + else { + WLOG(WS_LOG_DEBUG, "BUARMlDsaComposite: trad sign fail (%d)", + ret); + } + } + } + + if (ret == WS_SUCCESS) { + c32toa(LENGTH_SZ * 2 + authData->sf.publicKey.publicKeyTypeSz + sigSz, + output + begin); + begin += LENGTH_SZ; + + c32toa(authData->sf.publicKey.publicKeyTypeSz, output + begin); + begin += LENGTH_SZ; + WMEMCPY(output + begin, authData->sf.publicKey.publicKeyType, + authData->sf.publicKey.publicKeyTypeSz); + begin += authData->sf.publicKey.publicKeyTypeSz; + + c32toa(sigSz, output + begin); + begin += LENGTH_SZ; + WMEMCPY(output + begin, sig, sigSz); + begin += sigSz; + } + + if (ret == WS_SUCCESS) + *idx = begin; + + if (sig != NULL) { + WS_FORCEZERO(sig, sigSz); + WFREE(sig, keySig->heap, DYNTYPE_BUFFER); + } + if (checkData != NULL) { + WS_FORCEZERO(checkData, checkDataSz); + WFREE(checkData, keySig->heap, DYNTYPE_TEMP); + } + if (hash != NULL) { + WS_FORCEZERO(hash, params.tradHashSz); + WFREE(hash, keySig->heap, DYNTYPE_TEMP); + } + if (mPrime != NULL) { + WS_FORCEZERO(mPrime, mPrimeLen); + WFREE(mPrime, keySig->heap, DYNTYPE_TEMP); + } + + WLOG(WS_LOG_DEBUG, + "Leaving BuildUserAuthRequestMlDsaComposite(), ret = %d", ret); + return ret; +} + +static int DoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + WS_UserAuthData_PublicKey* pk, WS_UserAuthData* authData, + byte keyId, word32 pubKeyBlobSz) +{ + const byte* publicKeyType = NULL; + word32 publicKeyTypeSz = 0; + word32 pubRawSz = 0; + word32 sigSz = 0; + word32 i = 0; + int ret = WS_SUCCESS; + CompositeParams params; + WS_KeySignature* keySig = NULL; + + WLOG(WS_LOG_DEBUG, "Entering DoUserAuthRequestMlDsaComposite()"); + + if (ssh == NULL || ssh->ctx == NULL || pk == NULL || authData == NULL) { + return WS_BAD_ARGUMENT; + } + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + keySig = (WS_KeySignature*)WMALLOC(sizeof(WS_KeySignature), ssh->ctx->heap, + DYNTYPE_PUBKEY); + if (keySig == NULL) { + ret = WS_MEMORY_E; + } + else { + XMEMSET(keySig, 0, sizeof(*keySig)); + keySig->keyId = keyId; + keySig->heap = ssh->ctx->heap; + } + + if (ret == WS_SUCCESS) { + int mldsaInit = 0; + int tradInit = 0; + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + + ret = InitCompositeKeyPair(¶ms, &keySig->ks.mldsa_composite.mldsa, + &keySig->ks.mldsa_composite.trad, ops, keySig->heap, + &mldsaInit, &tradInit); + + if (ret == 0) { + ret = GetSize(&publicKeyTypeSz, pk->publicKey, pk->publicKeySz, &i); + } + if (ret == 0) { + publicKeyType = pk->publicKey + i; + i += publicKeyTypeSz; + if (publicKeyTypeSz != pk->publicKeyTypeSz + || WMEMCMP(publicKeyType, + pk->publicKeyType, publicKeyTypeSz) != 0) { + ret = WS_INVALID_ALGO_ID; + } + } + if (ret == 0) { + const byte* pubRawRef = NULL; + ret = GetStringRef(&pubRawSz, &pubRawRef, pk->publicKey, + pk->publicKeySz, &i); + if (ret == 0) { + if (pubRawSz != (params.mldsaPubSz + params.tradPubSz)) { + ret = WS_KEY_FORMAT_E; + } + } + if (ret == 0) { + ret = wc_MlDsaKey_ImportPubRaw( + &keySig->ks.mldsa_composite.mldsa, pubRawRef, + params.mldsaPubSz); + } + if (ret == 0) { + ret = ops->importPub(&keySig->ks.mldsa_composite.trad, + pubRawRef + params.mldsaPubSz, params.tradPubSz); + } + } + + if (ret != 0) { + if (mldsaInit) { + wc_MlDsaKey_Free(&keySig->ks.mldsa_composite.mldsa); + } + if (tradInit) { + ops->free(&keySig->ks.mldsa_composite.trad); + } + WFREE(keySig, ssh->ctx->heap, DYNTYPE_PUBKEY); + return WS_CRYPTO_FAILED; + } + } + + if (ret == WS_SUCCESS) { + i = 0; + ret = GetSize(&publicKeyTypeSz, pk->signature, pk->signatureSz, &i); + if (ret == WS_SUCCESS) { + publicKeyType = pk->signature + i; + i += publicKeyTypeSz; + if (publicKeyTypeSz != pk->publicKeyTypeSz + || WMEMCMP(publicKeyType, pk->publicKeyType, + publicKeyTypeSz) != 0) { + ret = WS_INVALID_ALGO_ID; + } + } + if (ret == WS_SUCCESS) { + ret = GetSize(&sigSz, pk->signature, pk->signatureSz, &i); + } + if (ret == WS_SUCCESS) { + word32 dataToSignSz = authData->usernameSz + + authData->serviceNameSz + + authData->authNameSz + BOOLEAN_SZ + + pk->publicKeyTypeSz + pubKeyBlobSz + + (UINT32_SZ * 5); + byte* checkData = (byte*)WMALLOC( + UINT32_SZ + ssh->sessionIdSz + MSG_ID_SZ + dataToSignSz, + ssh->ctx->heap, DYNTYPE_TEMP); + if (checkData == NULL) { + ret = WS_MEMORY_E; + } + else { + word32 idx = 0; + c32toa(ssh->sessionIdSz, checkData + idx); + idx += LENGTH_SZ; + WMEMCPY(checkData + idx, ssh->sessionId, ssh->sessionIdSz); + idx += ssh->sessionIdSz; + checkData[idx++] = MSGID_USERAUTH_REQUEST; + WMEMCPY(checkData + idx, pk->dataToSign, dataToSignSz); + + ret = VerifyMlDsaComposite(keySig->keyId, keySig->heap, + &keySig->ks.mldsa_composite.mldsa, + &keySig->ks.mldsa_composite.trad, + pk->signature + i, sigSz, checkData, + idx + dataToSignSz); + + WS_FORCEZERO(checkData, idx + dataToSignSz); + WFREE(checkData, ssh->ctx->heap, DYNTYPE_TEMP); + } + } + + wc_MlDsaKey_Free(&keySig->ks.mldsa_composite.mldsa); + { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + if (ops != NULL) { + ops->free(&keySig->ks.mldsa_composite.trad); + } + } + WFREE(keySig, ssh->ctx->heap, DYNTYPE_PUBKEY); + } + + return ret; +} +#endif + +#ifdef WOLFSSH_TEST_INTERNAL + +#ifndef WOLFSSH_NO_MLDSA +int wolfSSH_TestDoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + WS_UserAuthData* authData, byte keyId, word32 pubKeyBlobSz) +{ + if (authData == NULL) + return WS_BAD_ARGUMENT; + + return DoUserAuthRequestMlDsaComposite(ssh, &authData->sf.publicKey, + authData, + keyId, pubKeyBlobSz); +} + +int wolfSSH_TestPrepareUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + word32* payloadSz, const WS_UserAuthData* authData, + WS_KeySignature* keySig) +{ + return PrepareUserAuthRequestMlDsaComposite(ssh, payloadSz, authData, + keySig); +} + +/* exercises SignHMlDsaComposite() with a throwaway keypair; returns + * WS_SUCCESS or negative WS_* error code */ +int wolfSSH_TestSignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, + byte keyId) +{ + int ret; + CompositeParams params; + struct wolfSSH_sigKeyBlockFull sigKey; + const CompositeTradOps* ops; + + if (ssh == NULL || sig == NULL || sigSz == NULL) + return WS_BAD_ARGUMENT; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) + return ret; + + ops = WS_GetTradOps(params.tradType); + + WMEMSET(&sigKey, 0, sizeof(sigKey)); + sigKey.pubKeyId = keyId; + + ret = wc_MlDsaKey_Init(&sigKey.sk.mldsa_composite.base.mldsa, + ssh->ctx->heap, INVALID_DEVID); + if (ret == 0) + ret = wc_MlDsaKey_SetParams(&sigKey.sk.mldsa_composite.base.mldsa, + params.mldsaLevel); + if (ret == 0) + ret = wc_MlDsaKey_MakeKey(&sigKey.sk.mldsa_composite.base.mldsa, + ssh->rng); + if (ret != 0) { + wc_MlDsaKey_Free(&sigKey.sk.mldsa_composite.base.mldsa); + return WS_CRYPTO_FAILED; + } + + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->init(&sigKey.sk.mldsa_composite.base.trad, ssh->ctx->heap); + /* make_key not in CompositeTradOps: only this one call site needs it */ + 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 + } + } + } + + if (ret == 0) { + ret = SignHMlDsaComposite(ssh, sig, sigSz, &sigKey); + } + + wc_MlDsaKey_Free(&sigKey.sk.mldsa_composite.base.mldsa); + if (ops != NULL) { + ops->free(&sigKey.sk.mldsa_composite.base.trad); + } + + return ret; +} + +int wolfSSH_TestBuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + byte* output, word32* idx, const WS_UserAuthData* authData, + const byte* sigStart, word32 sigStartIdx, WS_KeySignature* keySig) +{ + return BuildUserAuthRequestMlDsaComposite(ssh, output, idx, authData, + sigStart, sigStartIdx, keySig); +} +#endif int wolfSSH_TestDoProtoId(WOLFSSH* ssh) { diff --git a/src/keygen.c b/src/keygen.c index 98d338ebf..3d9378e25 100644 --- a/src/keygen.c +++ b/src/keygen.c @@ -47,7 +47,16 @@ #ifndef WOLFSSH_NO_ECDSA #include #endif +#ifndef WOLFSSH_NO_ED25519 + #include +#endif +#ifdef HAVE_ED448 + #include +#endif +#include #include +#include +#include #ifdef WOLFSSH_KEYGEN @@ -330,6 +339,365 @@ int wolfSSH_MakeMlDsaKey(byte* out, word32 outSz, word32 level) #endif } + +/* Builds the OpenSSH-key-v1 envelope (composite keys have no ASN.1 + * form); see GetOpenSshKeyMlDsaComposite() in ossh.c for the parser. */ +#if !defined(WOLFSSH_NO_MLDSA) +/* Generates the traditional half of a composite key pair; exports raw + * material into the caller's buffers. */ +static int MakeCompositeTradKey(WC_RNG* rng, const CompositeParams* params, + byte* tradPub, byte* tradPriv) +{ + int ret = WS_NOT_COMPILED; + + WOLFSSH_UNUSED(rng); + WOLFSSH_UNUSED(tradPub); + WOLFSSH_UNUSED(tradPriv); + + 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); +#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); +#endif + } + else if (params->tradType == TRAD_TYPE_ECC) { +#ifndef WOLFSSH_NO_ECDSA + ecc_key key; + word32 sz; + /* Pin the curve explicitly: the draft requires P-256 for + * ML-DSA-44/65, P-384 for 87 -- key-size matching isn't guaranteed. */ + 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); +#endif + } + + return ret; +} +#endif /* !WOLFSSH_NO_MLDSA */ + +#if !defined(WOLFSSH_NO_MLDSA) +/* Base64_Encode() output size, mirrored from DoBase64_Encode(), so + * callers can size a buffer without a throwaway encode pass. */ +static word32 MlDsaCompositeBase64Sz(word32 fileSz) +{ + word32 chars = (fileSz + 2) / 3 * 4; + word32 lines = (chars + PEM_LINE_SZ - 1) / PEM_LINE_SZ; + + return chars + lines; +} +#endif /* !WOLFSSH_NO_MLDSA */ + +int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, + word32 tradType) +{ +#if !defined(WOLFSSH_NO_MLDSA) + static const char magic[] = "openssh-key-v1"; + static const char none[] = "none"; + const word32 noneSz = (word32)WSTRLEN(none); + const char* keyTypeName; + word32 keyTypeNameSz; + byte keyId; + CompositeParams params; + int ret; + WC_RNG rng; + int rngInit = 0; + MlDsaKey mldsaKey; + 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; + word32 fileSz, pubBlobSz, compositePubSz, compositePrivSz; + word32 privKeysStrSz, padSz, off, i, checkint; + + byte* tmpBuf = NULL; + word32 b64Sz = 0; + static const char header[] = "-----BEGIN OPENSSH PRIVATE KEY-----\n"; + static const char footer[] = "-----END OPENSSH PRIVATE KEY-----\n"; + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_MakeMlDsaCompositeKey()"); + + if (level == WOLFSSH_MLDSAKEY_44 && + tradType == WOLFSSH_COMPOSITE_TRAD_ED25519) + keyId = ID_MLDSA44_ED25519; + else if (level == WOLFSSH_MLDSAKEY_44 && + tradType == WOLFSSH_COMPOSITE_TRAD_ECDSA) + keyId = ID_MLDSA44_ES256; + else if (level == WOLFSSH_MLDSAKEY_65 && + tradType == WOLFSSH_COMPOSITE_TRAD_ED25519) + keyId = ID_MLDSA65_ED25519; + else if (level == WOLFSSH_MLDSAKEY_65 && + tradType == WOLFSSH_COMPOSITE_TRAD_ECDSA) + keyId = ID_MLDSA65_ES256; + else if (level == WOLFSSH_MLDSAKEY_87 && + tradType == WOLFSSH_COMPOSITE_TRAD_ED448) + keyId = ID_MLDSA87_ED448; + else if (level == WOLFSSH_MLDSAKEY_87 && + tradType == WOLFSSH_COMPOSITE_TRAD_ECDSA) + keyId = ID_MLDSA87_ES384; + else { + WLOG(WS_LOG_DEBUG, "Invalid ML-DSA composite level/trad combination"); + return WS_BAD_ARGUMENT; + } + + if (WS_GetCompositeParams(keyId, ¶ms) != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "Composite algorithm not compiled in"); + return WS_NOT_COMPILED; + } + + keyTypeName = IdToName(keyId); + keyTypeNameSz = (word32)WSTRLEN(keyTypeName); + + /* final base64 size vs. outSz is checked below, after PEM body build */ + compositePubSz = params.mldsaPubSz + params.tradPubSz; + compositePrivSz = MLDSA_SEED_SZ + params.tradPrivSz; + + pubBlobSz = UINT32_SZ + keyTypeNameSz + UINT32_SZ + compositePubSz; + privKeysStrSz = UINT32_SZ * 2 /* checkints */ + + UINT32_SZ + keyTypeNameSz + + UINT32_SZ + compositePubSz + + UINT32_SZ + compositePrivSz + + UINT32_SZ /* comment (empty) */; + padSz = (MIN_BLOCK_SZ - (privKeysStrSz % MIN_BLOCK_SZ)) % MIN_BLOCK_SZ; + privKeysStrSz += padSz; + + fileSz = (word32)WSTRLEN(magic) + 1 + + UINT32_SZ + noneSz /* ciphername */ + + UINT32_SZ + noneSz /* kdfname */ + + UINT32_SZ /* kdfoptions (empty) */ + + UINT32_SZ /* keycount */ + + UINT32_SZ + pubBlobSz + + UINT32_SZ + privKeysStrSz; + + /* the encoded size depends only on fileSz, so it (and outSz + * feasibility) can be determined before doing any RNG/keygen work */ + b64Sz = MlDsaCompositeBase64Sz(fileSz); + + if (out == NULL) { + /* size query: caller wants the required buffer size only */ + return (int)(WSTRLEN(header) + b64Sz + WSTRLEN(footer) + 1); + } + if (outSz < b64Sz + WSTRLEN(header) + WSTRLEN(footer) + 1) { + WLOG(WS_LOG_DEBUG, "Output buffer too small for composite key"); + return WS_BUFFER_E; + } + + ret = wc_InitRng(&rng); + if (ret != 0) { + WLOG(WS_LOG_DEBUG, "Couldn't create RNG"); + return WS_CRYPTO_FAILED; + } + rngInit = 1; + + ret = wc_RNG_GenerateBlock(&rng, mldsaSeed, sizeof(mldsaSeed)); + if (ret != 0) { + ret = WS_CRYPTO_FAILED; + } + else { + 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) { + WLOG(WS_LOG_DEBUG, "Couldn't generate ML-DSA half of composite key"); + } + mldsaGenOk = (ret == 0); + + if (ret == 0) { + ret = MakeCompositeTradKey(&rng, ¶ms, tradPub, tradPriv); + } + if (ret != 0 && mldsaGenOk) { + WLOG(WS_LOG_DEBUG, + "Couldn't generate traditional half of composite key"); + } + + if (ret == 0 && wc_RNG_GenerateBlock(&rng, (byte*)&checkint, + sizeof(checkint)) != 0) { + ret = WS_CRYPTO_FAILED; + } + + if (ret == 0) { + tmpBuf = (byte*)WMALLOC(fileSz, NULL, DYNTYPE_BUFFER); + if (tmpBuf == NULL) { + ret = WS_MEMORY_E; + } + } + + if (ret == 0) { + off = 0; + WMEMCPY(tmpBuf + off, magic, WSTRLEN(magic) + 1); + off += (word32)WSTRLEN(magic) + 1; + c32toa(noneSz, tmpBuf + off); off += UINT32_SZ; + WMEMCPY(tmpBuf + off, none, noneSz); off += noneSz; + c32toa(noneSz, tmpBuf + off); off += UINT32_SZ; + WMEMCPY(tmpBuf + off, none, noneSz); off += noneSz; + c32toa(0, tmpBuf + off); off += UINT32_SZ; /* kdfoptions */ + c32toa(1, tmpBuf + off); off += UINT32_SZ; /* keycount */ + + c32toa(pubBlobSz, tmpBuf + off); off += UINT32_SZ; + c32toa(keyTypeNameSz, tmpBuf + off); off += UINT32_SZ; + WMEMCPY(tmpBuf + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + c32toa(compositePubSz, tmpBuf + off); off += UINT32_SZ; + WMEMCPY(tmpBuf + off, mldsaPub, params.mldsaPubSz); + off += params.mldsaPubSz; + WMEMCPY(tmpBuf + off, tradPub, params.tradPubSz); + off += params.tradPubSz; + + c32toa(privKeysStrSz, tmpBuf + off); off += UINT32_SZ; + c32toa(checkint, tmpBuf + off); off += UINT32_SZ; + c32toa(checkint, tmpBuf + off); off += UINT32_SZ; + c32toa(keyTypeNameSz, tmpBuf + off); off += UINT32_SZ; + WMEMCPY(tmpBuf + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + c32toa(compositePubSz, tmpBuf + off); off += UINT32_SZ; + WMEMCPY(tmpBuf + off, mldsaPub, params.mldsaPubSz); + off += params.mldsaPubSz; + WMEMCPY(tmpBuf + off, tradPub, params.tradPubSz); + off += params.tradPubSz; + c32toa(compositePrivSz, tmpBuf + off); off += UINT32_SZ; + WMEMCPY(tmpBuf + off, mldsaSeed, MLDSA_SEED_SZ); off += MLDSA_SEED_SZ; + WMEMCPY(tmpBuf + off, tradPriv, params.tradPrivSz); + off += params.tradPrivSz; + c32toa(0, tmpBuf + off); off += UINT32_SZ; /* comment (empty) */ + for (i = 1; i <= padSz; i++) { + tmpBuf[off++] = (byte)i; + } + + if (off != fileSz) { + ret = WS_CRYPTO_FAILED; + } + else { + /* outSz feasibility for this exact b64Sz was already verified + * against the deterministic size probe above */ + off = 0; + WMEMCPY(out + off, header, WSTRLEN(header)); + off += (word32)WSTRLEN(header); + if (Base64_Encode(tmpBuf, fileSz, out + off, &b64Sz) == 0) { + off += b64Sz; + WMEMCPY(out + off, footer, WSTRLEN(footer)); + off += (word32)WSTRLEN(footer); + /* out is PEM text of exactly `off` bytes; NUL-terminate + * only if the caller's buffer has room to spare (which it + * must, due to the size check at the start of this function) */ + if (outSz > off) { + out[off] = '\0'; + } + ret = (int)(off + 1); + } + else ret = WS_CRYPTO_FAILED; + } + } + + if (mldsaInit) wc_MlDsaKey_Free(&mldsaKey); + if (rngInit) wc_FreeRng(&rng); + + WS_FORCEZERO(mldsaSeed, sizeof(mldsaSeed)); + WS_FORCEZERO(tradPriv, sizeof(tradPriv)); + + if (tmpBuf != NULL) { + WS_FORCEZERO(tmpBuf, fileSz); + WFREE(tmpBuf, NULL, DYNTYPE_BUFFER); + } + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_MakeMlDsaCompositeKey(), ret = %d", + ret); + return ret; +#else + WOLFSSH_UNUSED(out); + WOLFSSH_UNUSED(outSz); + WOLFSSH_UNUSED(level); + WOLFSSH_UNUSED(tradType); + return WS_NOT_COMPILED; +#endif +} + #else /* WOLFSSL_KEY_GEN */ #error "wolfSSH keygen requires that keygen is enabled in wolfSSL, use --enable-keygen or #define WOLFSSL_KEY_GEN." #endif /* WOLFSSL_KEY_GEN */ diff --git a/src/ossh.c b/src/ossh.c index faf2a68e9..030200230 100644 --- a/src/ossh.c +++ b/src/ossh.c @@ -297,6 +297,93 @@ static int GetOpenSshKeyMlDsa(MlDsaKey* key, } return ret; } + +/* Parse OpenSSH ML-DSA composite private key blob. + * Returns WS_SUCCESS or negative WS_* error. */ +static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, + void* tradKey, void* heap, const byte* buf, word32 len, word32* idx) +{ + const byte *pub = NULL; + const byte *priv = NULL; + word32 pubSz = 0; + word32 privSz = 0; + int ret; + int mldsaInit = 0; + int tradInit = 0; + CompositeParams params; + const CompositeTradOps* ops; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + ops = WS_GetTradOps(params.tradType); + + ret = InitCompositeKeyPair(¶ms, mldsa, tradKey, ops, heap, + &mldsaInit, &tradInit); + if (ret != 0) { + if (mldsaInit) wc_MlDsaKey_Free(mldsa); + if (tradInit) ops->free(tradKey); + return (ret == WS_UNIMPLEMENTED_E) ? ret : WS_CRYPTO_FAILED; + } + + ret = GetStringRef(&pubSz, &pub, buf, len, idx); + if (ret == WS_SUCCESS) + ret = GetStringRef(&privSz, &priv, buf, len, idx); + + if (ret == WS_SUCCESS) { + word32 expectedPrivSz = MLDSA_SEED_SZ + params.tradPrivSz; + + if (pubSz != (params.mldsaPubSz + params.tradPubSz) || + privSz != expectedPrivSz) { + ret = WS_KEY_FORMAT_E; + } + } + + if (ret == WS_SUCCESS) { + 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]; +#else + byte mldsaPubCheck[WC_MLDSA_44_PUB_KEY_SIZE]; +#endif + word32 mldsaPubCheckSz = sizeof(mldsaPubCheck); + + 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 = ops->importPriv(tradKey, priv + MLDSA_SEED_SZ, params.tradPrivSz, + pub + params.mldsaPubSz, params.tradPubSz); + } + if (ret == WS_SUCCESS) { + byte tradPubCheck[COMPOSITE_MAX_TRAD_PUB_SZ]; + word32 tradPubCheckSz = sizeof(tradPubCheck); + + ret = ops->exportPub(tradKey, tradPubCheck, &tradPubCheckSz); + if (ret == 0 && + (tradPubCheckSz != params.tradPubSz || + WMEMCMP(tradPubCheck, pub + params.mldsaPubSz, + params.tradPubSz) != 0)) { + ret = WS_KEY_FORMAT_E; + } + } + + if (ret != 0) { + wc_MlDsaKey_Free(mldsa); + ops->free(tradKey); + ret = WS_KEY_FORMAT_E; + } + return ret; +} #endif #ifdef WOLFSSH_TPM @@ -357,6 +444,8 @@ static int GetOpenSshKeyPublicMlDsa(MlDsaKey* key, const byte* buf, } return ret; } + + #endif #ifndef WOLFSSH_NO_RSA static int GetOpenSshPublicKeyRsa(RsaKey* key, const byte* buf, word32 len, @@ -423,6 +512,7 @@ int GetOpenSshPublicKey(WS_KeySignature *key, ret = GetOpenSshKeyPublicMlDsa(&key->ks.mldsa.key, buf, len, idx, WC_ML_DSA_87); break; + #endif default: ret = WS_UNIMPLEMENTED_E; @@ -440,11 +530,12 @@ int GetOpenSshKey(WS_KeySignature *key, const byte* buf, word32 len, word32* idx) { const char AuthMagic[] = "openssh-key-v1"; + const word32 authMagicSz = (word32)WSTRLEN(AuthMagic) + 1; /* incl NUL */ const byte* str = NULL; word32 keyCount = 0, strSz, i; int ret = WS_SUCCESS; - if (WSTRCMP(AuthMagic, (const char*)buf) != 0) { + if (len < authMagicSz || WMEMCMP(AuthMagic, buf, authMagicSz) != 0) { ret = WS_KEY_AUTH_MAGIC_E; } @@ -532,16 +623,41 @@ int GetOpenSshKey(WS_KeySignature *key, ret = GetOpenSshKeyMlDsa( &key->ks.mldsa.key, str, strSz, &subIdx, WC_ML_DSA_44); + /* clear keyId: key already freed, avoid + * double free */ + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; break; case ID_MLDSA65: ret = GetOpenSshKeyMlDsa( &key->ks.mldsa.key, str, strSz, &subIdx, WC_ML_DSA_65); + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; break; case ID_MLDSA87: ret = GetOpenSshKeyMlDsa( &key->ks.mldsa.key, str, strSz, &subIdx, WC_ML_DSA_87); + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; + break; + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: + case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: + ret = GetOpenSshKeyMlDsaComposite( + key->keyId, + &key->ks.mldsa_composite.mldsa, + &key->ks.mldsa_composite.trad, + key->heap, + str, strSz, &subIdx); + /* clear keyId: key already freed, avoid + * double free */ + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; break; #endif default: @@ -567,7 +683,9 @@ int GetOpenSshKey(WS_KeySignature *key, check1 <= check2; check1++, subIdx++) { if (check1 != str[subIdx]) { - /* Bad pad value. */ + /* bad pad: free key decoded above */ + wolfSSH_KEY_clean(key); + key->keyId = ID_NONE; ret = WS_KEY_FORMAT_E; break; } diff --git a/src/ssh.c b/src/ssh.c index fa7bfb60f..12fbf9c46 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1796,11 +1796,9 @@ union wolfSSH_key { #endif }; -static const char* PrivBeginOpenSSH = "-----BEGIN OPENSSH PRIVATE KEY-----"; -static const char* PrivEndOpenSSH = "-----END OPENSSH PRIVATE KEY-----"; - #if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) /* currently only used in wolfSSH_ReadKey_file() */ + static const char* PrivBeginOpenSSH = "-----BEGIN OPENSSH PRIVATE KEY-----"; static const char* PrivBeginPrefix = "-----BEGIN "; /* static const char* PrivEndPrefix = "-----END "; */ static const char* PrivSuffix = " PRIVATE KEY-----"; @@ -2142,36 +2140,16 @@ static int DoOpenSshKey(const byte* in, word32 inSz, byte** out, int ret = WS_SUCCESS; byte* newKey = NULL; word32 newKeySz = inSz; /* binary will be smaller than PEM */ - word32 beginSz = (word32)WSTRLEN(PrivBeginOpenSSH); - word32 endSz = (word32)WSTRLEN(PrivEndOpenSSH); - const byte* b64 = NULL; - const char* footer = NULL; - word32 b64Sz = 0; - - /* Reject buffers too small to hold both markers. Without this guard the - * subtraction used to locate the base64 region underflows inSz. */ - if (inSz <= beginSz + endSz) { - WLOG(WS_LOG_DEBUG, "OpenSSH private key buffer too small"); - return WS_PARSE_E; - } + const char* beginMarker = "-----BEGIN OPENSSH PRIVATE KEY-----"; + const char* endMarker = "-----END OPENSSH PRIVATE KEY-----"; + word32 beginSz = (word32)WSTRLEN(beginMarker); + word32 endSz = (word32)WSTRLEN(endMarker); - /* The begin marker must lead the buffer. */ - if (WMEMCMP(in, PrivBeginOpenSSH, beginSz) != 0) { - WLOG(WS_LOG_DEBUG, "OpenSSH private key missing begin marker"); + if (inSz <= beginSz + endSz || WMEMCMP(in, beginMarker, beginSz) != 0 || + WSTRNSTR((const char*)in + beginSz, endMarker, inSz - beginSz) == NULL) { return WS_PARSE_E; } - /* Locate the end marker so the base64 region is bounded by the input. */ - footer = WSTRNSTR((const char*)in + beginSz, PrivEndOpenSSH, - inSz - beginSz); - if (footer == NULL) { - WLOG(WS_LOG_DEBUG, "OpenSSH private key missing end marker"); - return WS_PARSE_E; - } - - b64 = in + beginSz; - b64Sz = (word32)(footer - (const char*)b64); - if (*out == NULL) { newKey = (byte*)WMALLOC(newKeySz, heap, DYNTYPE_PRIVKEY); if (newKey == NULL) { @@ -2187,17 +2165,18 @@ static int DoOpenSshKey(const byte* in, word32 inSz, byte** out, newKeySz = *outSz; } - ret = Base64_Decode((byte*)b64, b64Sz, newKey, &newKeySz); - if (ret == 0) { - ret = WS_SUCCESS; - } - else { - WLOG(WS_LOG_DEBUG, "Base64 decode of public key failed."); - ret = WS_PARSE_E; + /* locates the begin/end markers and base64-decodes the block between + * them; shared with wolfSSH_ProcessBuffer()'s OPENSSH format path */ + ret = WS_StripOpenSshPem(in, inSz, newKey, &newKeySz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "OpenSSH private key marker/decode failed."); } if (ret == WS_SUCCESS) { ret = IdentifyOpenSshKey(newKey, newKeySz, heap); + if (ret <= 0) { + WLOG(WS_LOG_DEBUG, "Unable to identify key"); + } } if (ret > 0) { @@ -2208,7 +2187,6 @@ static int DoOpenSshKey(const byte* in, word32 inSz, byte** out, ret = WS_SUCCESS; } else { - WLOG(WS_LOG_DEBUG, "Unable to identify key"); WS_FORCEZERO(newKey, newKeySz); if (*out == NULL) { WFREE(newKey, heap, DYNTYPE_PRIVKEY); diff --git a/tests/kex.c b/tests/kex.c index b08560cf7..376eb5df5 100644 --- a/tests/kex.c +++ b/tests/kex.c @@ -515,6 +515,37 @@ int wolfSSH_KexTest(int argc, char** argv) #ifndef WOLFSSH_NO_MLDSA87 AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa-87"), EXIT_SUCCESS); #endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + /* Uses the "@openssh.com" wire name that OpenSSH negotiates for this + * algorithm, matching what wolfSSH now emits (see NameIdMap). */ + AssertIntEQ( + wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa44-ed25519@openssh.com"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa44-es256@wolfssl.com"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa65-es256@wolfssl.com"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa65-ed25519@wolfssl.com"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa87-es384@wolfssl.com"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa87-ed448@wolfssl.com"), + EXIT_SUCCESS); +#endif AssertIntEQ(wolfSSH_Cleanup(), WS_SUCCESS); diff --git a/tests/unit.c b/tests/unit.c index 8861f87b2..3c64c19a7 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -898,7 +898,18 @@ static int test_MlDsaKeyGen(void) printf("MlDsaKeyGen: MakeMlDsaKey level %s failed (%d)\n", params[i].name, sz); WFREE(der, NULL, DYNTYPE_BUFFER); - result = -106; + result = -107; + break; + } + /* Confirm the DER size constant is exact, not merely an upper + * bound. This is what makes the derSz - 1 undersized-buffer test + * below a meaningful tight boundary check rather than one that + * could incidentally pass against a generous constant. */ + if ((word32)sz != params[i].derSz) { + printf("MlDsaKeyGen: level %s DER size %d != constant %u\n", + params[i].name, sz, params[i].derSz); + WFREE(der, NULL, DYNTYPE_BUFFER); + result = -108; break; } @@ -907,7 +918,7 @@ static int test_MlDsaKeyGen(void) printf("MlDsaKeyGen: undersized buffer wrong result %d, level %s\n", sz, params[i].name); WFREE(der, NULL, DYNTYPE_BUFFER); - result = -107; + result = -109; break; } @@ -918,15 +929,273 @@ static int test_MlDsaKeyGen(void) int sz = wolfSSH_MakeMlDsaKey(NULL, 0, 9999); if (sz != WS_BAD_ARGUMENT) { printf("MlDsaKeyGen: invalid level wrong result %d\n", sz); - result = -108; + result = -110; } } return result; } -#endif -#endif +/* Generates a composite key with wolfSSH_MakeMlDsaCompositeKey() for every + * compiled-in ML-DSA level/traditional-algo combo, then round-trips it + * through the public wolfSSH_CTX_UsePrivateKey_buffer() OpenSSH-format + * parser to confirm the two agree on the on-disk envelope layout. */ +static int test_MlDsaCompositeKeyGen(void) +{ + /* NULL-terminated so the table is never empty if ECDSA and + * Ed25519/Ed448 are all disabled while ML-DSA is enabled */ + static const struct { + word32 level; + word32 tradType; + const char* name; + } params[] = { + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { WOLFSSH_MLDSAKEY_44, WOLFSSH_COMPOSITE_TRAD_ED25519, "44+Ed25519" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + { WOLFSSH_MLDSAKEY_44, WOLFSSH_COMPOSITE_TRAD_ECDSA, "44+ES256" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { WOLFSSH_MLDSAKEY_65, WOLFSSH_COMPOSITE_TRAD_ED25519, "65+Ed25519" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + { WOLFSSH_MLDSAKEY_65, WOLFSSH_COMPOSITE_TRAD_ECDSA, "65+ES256" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + { WOLFSSH_MLDSAKEY_87, WOLFSSH_COMPOSITE_TRAD_ED448, "87+Ed448" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + { WOLFSSH_MLDSAKEY_87, WOLFSSH_COMPOSITE_TRAD_ECDSA, "87+ES384" }, + #endif + { 0, 0, NULL } + }; + const word32 bufSz = 8192; + word32 i; + word32 firstLevel = 0; + word32 firstTradType = 0; + word32 firstSz = 0; + int result = 0; + + for (i = 0; i < (word32)(sizeof(params) / sizeof(params[0])) && + params[i].name != NULL; i++) { + WOLFSSH_CTX* ctx; + byte* buf; + int sz; + + buf = (byte*)WMALLOC(bufSz, NULL, DYNTYPE_BUFFER); + if (buf == NULL) { + printf("MlDsaCompositeKeyGen: alloc failed for %s\n", + params[i].name); + result = -120; + break; + } + + sz = wolfSSH_MakeMlDsaCompositeKey(buf, bufSz, params[i].level, + params[i].tradType); + if (sz < 0) { + printf("MlDsaCompositeKeyGen: MakeMlDsaCompositeKey %s " + "failed (%d)\n", params[i].name, sz); + WFREE(buf, NULL, DYNTYPE_BUFFER); + result = -121; + break; + } + + if (i == 0) { + firstLevel = params[i].level; + firstTradType = params[i].tradType; + firstSz = (word32)sz; + } + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { + printf("MlDsaCompositeKeyGen: CTX_new failed for %s\n", + params[i].name); + WFREE(buf, NULL, DYNTYPE_BUFFER); + result = -122; + break; + } + + if (wolfSSH_CTX_UsePrivateKey_buffer(ctx, buf, (word32)sz, + WOLFSSH_FORMAT_OPENSSH) != WS_SUCCESS) { + printf("MlDsaCompositeKeyGen: round-trip parse failed for %s\n", + params[i].name); + result = -123; + } + + wolfSSH_CTX_free(ctx); + WFREE(buf, NULL, DYNTYPE_BUFFER); + if (result != 0) { + break; + } + } + + if (result == 0) { + /* out == NULL is a size query for a valid (level, tradType): it + * should return the required buffer size, not an error. */ + int sz = wolfSSH_MakeMlDsaCompositeKey(NULL, 0, firstLevel, + firstTradType); + if (sz <= 0 || (word32)sz != firstSz) { + printf("MlDsaCompositeKeyGen: NULL out size query wrong " + "result %d (expected %u)\n", sz, firstSz); + result = -124; + } + } + + if (result == 0) { + byte dummy[1]; + int sz = wolfSSH_MakeMlDsaCompositeKey(dummy, sizeof(dummy), 9999, + 9999); + if (sz != WS_BAD_ARGUMENT) { + printf("MlDsaCompositeKeyGen: invalid level/tradType wrong " + "result %d\n", sz); + result = -125; + } + } + + if (result == 0) { + byte dummy[1]; + int sz = wolfSSH_MakeMlDsaCompositeKey(dummy, sizeof(dummy), + WOLFSSH_MLDSAKEY_87, WOLFSSH_COMPOSITE_TRAD_ED25519); + if (sz != WS_BAD_ARGUMENT) { + printf("MlDsaCompositeKeyGen: mismatched level/tradType wrong " + "result %d\n", sz); + result = -127; + } + } + + /* firstLevel/firstTradType/firstSz are only populated when params[] has + * at least one entry for this build's enabled algorithms; skip the + * undersized-buffer check (which relies on a valid firstSz) otherwise. */ + if (result == 0 && firstSz > 0) { + byte* buf = (byte*)WMALLOC(firstSz, NULL, DYNTYPE_BUFFER); + + if (buf == NULL) { + printf("MlDsaCompositeKeyGen: alloc failed for undersized " + "buffer test\n"); + result = -126; + } + else { + int sz = wolfSSH_MakeMlDsaCompositeKey(buf, firstSz - 1, + firstLevel, firstTradType); + if (sz != WS_BUFFER_E) { + printf("MlDsaCompositeKeyGen: undersized buffer wrong " + "result %d\n", sz); + result = -128; + } + WFREE(buf, NULL, DYNTYPE_BUFFER); + } + } + + return result; +} + +#endif /* WOLFSSH_NO_MLDSA */ + +#endif /* WOLFSSH_KEYGEN */ + +/* Exercises the malformed-input error paths of the WOLFSSH_FORMAT_OPENSSH + * PEM stripping added to wolfSSH_ProcessBuffer() (via the public + * wolfSSH_CTX_UsePrivateKey_buffer() entry point): a header that doesn't + * match "-----BEGIN OPENSSH PRIVATE KEY-----", a missing footer, and a + * corrupt/non-Base64 body. Each should fail with WS_BAD_FILE_E rather than + * crash or leak. */ +static int test_OpenSshPemNegative(void) +{ + static const char badHeader[] = + "-----BEGIN OPENSSH PUBLIC KEY-----\n" + "AAAA\n" + "-----END OPENSSH PRIVATE KEY-----\n"; + static const char noFooter[] = + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"; + static const char badBase64[] = + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "not valid base64 !!!\n" + "-----END OPENSSH PRIVATE KEY-----\n"; + static const struct { + const char* pem; + word32 pemSz; + const char* name; + } cases[] = { + { badHeader, (word32)sizeof(badHeader) - 1, "bad header" }, + { noFooter, (word32)sizeof(noFooter) - 1, "missing footer" }, + { badBase64, (word32)sizeof(badBase64) - 1, "bad base64" }, + }; + word32 i; + int result = 0; + + for (i = 0; i < (word32)(sizeof(cases) / sizeof(cases[0])); i++) { + WOLFSSH_CTX* ctx; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { + printf("OpenSshPemNegative: CTX_new failed for %s\n", + cases[i].name); + result = -116; + break; + } + + ret = wolfSSH_CTX_UsePrivateKey_buffer(ctx, + (const byte*)cases[i].pem, cases[i].pemSz, + WOLFSSH_FORMAT_OPENSSH); + if (ret != WS_BAD_FILE_E) { + printf("OpenSshPemNegative: %s wrong result %d\n", + cases[i].name, ret); + result = -117; + } + + wolfSSH_CTX_free(ctx); + if (result != 0) { + break; + } + } + + return result; +} + + +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 +/* A well-formed OpenSSH-format key that isn't a composite type must be + * rejected, not stored as an envelope no non-composite parser can walk. */ +static int test_OpenSshFormatNonCompositeRejected(void) +{ + static const char ecdsaOpenSsh[] = + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS\n" + "1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTAqdBgCp8bYSq2kQQ48/Ud8Iy6Mjnb\n" + "/fpB3LfSE/1kx9VaaE4FL3i9Gg2vDV0eLGM3PWksFNPhULxtcYJyjaBjAAAAqJAeleSQHp\n" + "XkAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMCp0GAKnxthKraR\n" + "BDjz9R3wjLoyOdv9+kHct9IT/WTH1VpoTgUveL0aDa8NXR4sYzc9aSwU0+FQvG1xgnKNoG\n" + "MAAAAgPrOgktioNqad/wHNC/rt/zVrpNqDnOwg9tNDFMOTwo8AAAANYm9iQGxvY2FsaG9z\n" + "dAECAw==\n" + "-----END OPENSSH PRIVATE KEY-----\n"; + WOLFSSH_CTX* ctx; + int ret; + int result = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { + printf("OpenSshFormatNonCompositeRejected: CTX_new failed\n"); + return -118; + } + + ret = wolfSSH_CTX_UsePrivateKey_buffer(ctx, + (const byte*)ecdsaOpenSsh, (word32)sizeof(ecdsaOpenSsh) - 1, + WOLFSSH_FORMAT_OPENSSH); + if (ret != WS_UNIMPLEMENTED_E) { + printf("OpenSshFormatNonCompositeRejected: wrong result %d\n", ret); + result = -119; + } + + wolfSSH_CTX_free(ctx); + return result; +} +#endif /* !WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ #if defined(WOLFSSH_TEST_INTERNAL) && \ @@ -8225,113 +8494,1239 @@ static int test_DoUserAuthRequestMlDsa(void) return 0; } -#ifdef WOLFSSH_KEYGEN -static int test_PrepareUserAuthRequestMlDsa(void) +/* tamperSig: 0 = no tampering, 1 = flip a byte in the trad-algorithm + * signature component, 2 = flip a byte in the ML-DSA signature component, + * 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. */ +static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, + byte keyId, int tamperSig) { - int ret = 0; -#ifndef WOLFSSH_NO_MLDSA44 - ret = test_PrepareUserAuthRequestMlDsa_Params(WOLFSSH_MLDSAKEY_44, - ID_MLDSA44, WC_MLDSA_44_BOTH_KEY_DER_SIZE); - if (ret != 0) return ret; -#endif -#ifndef WOLFSSH_NO_MLDSA65 - ret = test_PrepareUserAuthRequestMlDsa_Params(WOLFSSH_MLDSAKEY_65, - ID_MLDSA65, WC_MLDSA_65_BOTH_KEY_DER_SIZE); - if (ret != 0) return ret; -#endif -#ifndef WOLFSSH_NO_MLDSA87 - ret = test_PrepareUserAuthRequestMlDsa_Params(WOLFSSH_MLDSAKEY_87, - ID_MLDSA87, WC_MLDSA_87_BOTH_KEY_DER_SIZE); - if (ret != 0) return ret; -#endif - (void)ret; - return 0; -} + static const char username[] = "wolfssh"; + static const char serviceName[] = "ssh-connection"; + static const char authName[] = "publickey"; + const word32 keyTypeNameSz = (word32)(WSTRLEN(keyTypeName)); + const word32 usernameSz = (word32)(sizeof(username) - 1); + const word32 serviceNameSz = (word32)(sizeof(serviceName) - 1); + const word32 authNameSz = (word32)(sizeof(authName) - 1); -#ifdef WOLFSSH_CERTS -static int test_PrepareUserAuthRequestMlDsaCert_Params(word32 keygenLevel, - byte keyId, int derBufSz) -{ WOLFSSH_CTX* ctx = NULL; WOLFSSH* ssh = NULL; + MlDsaKey signingKey; + int signingKeyInit = 0; +#ifndef WOLFSSH_NO_ECDSA + ecc_key eccKey; + int eccInit = 0; +#endif +#ifndef WOLFSSH_NO_ED25519 + ed25519_key ed25519Key; + int ed25519Init = 0; +#endif +#ifdef HAVE_ED448 + ed448_key ed448Key; + int ed448Init = 0; +#endif + WC_RNG rng; + int rngInit = 0; WS_UserAuthData authData; - WS_KeySignature keySig; - byte* derKey = NULL; - word32 payloadSz; - int derKeySz; + CompositeParams params; + + byte* pubKeyBlob = NULL; + byte* sigBlob = NULL; + byte* dataToSign = NULL; + byte* checkData = NULL; + byte* pubRaw = NULL; + byte* tradPub = NULL; + byte* mldsaSig = NULL; + byte* tradSig = NULL; + byte* hash = NULL; + byte* m_prime = NULL; + + word32 pubKeyBlobSz = 0; + word32 sigBlobSz = 0; + word32 dataToSignSz = 0; + word32 checkDataSz = 0; + + word32 off; + word32 mldsaSigSz; + word32 tradSigSz = 0; + word32 m_prime_len; int result = 0; int ret; - ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); - if (ctx == NULL) return -920; + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) return -700; ssh = wolfSSH_new(ctx); - if (ssh == NULL) { result = -921; goto done; } + if (ssh == NULL) { result = -701; goto done; } - derKey = (byte*)WMALLOC(derBufSz, NULL, 0); - if (derKey == NULL) { result = -922; goto done; } + /* Stub a session id so the verify hash has something to absorb. */ + ssh->sessionIdSz = 16; + WMEMSET(ssh->sessionId, 0xA5, ssh->sessionIdSz); - derKeySz = wolfSSH_MakeMlDsaKey(derKey, (word32)derBufSz, keygenLevel); - if (derKeySz < 0) { result = -923; goto done; } + if (wc_InitRng(&rng) != 0) { + result = -702; + goto done; + } + rngInit = 1; - /* Success path: good key, hasSignature=0 */ - WMEMSET(&authData, 0, sizeof(authData)); - WMEMSET(&keySig, 0, sizeof(keySig)); - payloadSz = 0; - authData.sf.publicKey.privateKey = derKey; - authData.sf.publicKey.privateKeySz = (word32)derKeySz; - authData.sf.publicKey.hasSignature = 0; - keySig.keyId = keyId; - keySig.heap = NULL; - ret = wolfSSH_TestPrepareUserAuthRequestMlDsaCert(ssh, &payloadSz, - &authData, &keySig); - if (ret != WS_SUCCESS) { result = -924; goto done; } - wc_MlDsaKey_Free(&keySig.ks.mldsa.key); + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) { result = -703; goto done; } - /* Bad key: exercises the PrivateKeyDecode failure free path */ - { - static const byte badKey[] = { 0xFF, 0xFE, 0x00, 0x01 }; - WMEMSET(&authData, 0, sizeof(authData)); - WMEMSET(&keySig, 0, sizeof(keySig)); - payloadSz = 0; - authData.sf.publicKey.privateKey = badKey; - authData.sf.publicKey.privateKeySz = sizeof(badKey); - authData.sf.publicKey.hasSignature = 0; - keySig.keyId = keyId; - keySig.heap = NULL; - ret = wolfSSH_TestPrepareUserAuthRequestMlDsaCert(ssh, &payloadSz, - &authData, &keySig); - if (ret == WS_SUCCESS) { result = -925; goto done; } + if (wc_MlDsaKey_Init(&signingKey, NULL, + INVALID_DEVID) != 0) { result = -704; goto done; } + signingKeyInit = 1; + if (wc_MlDsaKey_SetParams(&signingKey, + params.mldsaLevel) != 0) { result = -705; goto done; } + if (wc_MlDsaKey_MakeKey(&signingKey, + &rng) != 0) { result = -706; goto done; } + + if (params.tradType == TRAD_TYPE_ED25519) { +#ifndef WOLFSSH_NO_ED25519 + if (wc_ed25519_init(&ed25519Key) != 0) { result = -707; goto done; } + ed25519Init = 1; + if (wc_ed25519_make_key(&rng, ED25519_KEY_SIZE, + &ed25519Key) != 0) { result = -708; goto done; } +#else + result = -709; goto done; +#endif + } + else if (params.tradType == TRAD_TYPE_ED448) { +#ifdef HAVE_ED448 + if (wc_ed448_init(&ed448Key) != 0) { result = -710; goto done; } + ed448Init = 1; + if (wc_ed448_make_key(&rng, 57, + &ed448Key) != 0) { result = -711; goto done; } +#else + result = -712; goto done; +#endif + } + else if (params.tradType == TRAD_TYPE_ECC) { +#ifndef WOLFSSH_NO_ECDSA + if (wc_ecc_init(&eccKey) != 0) { result = -713; goto done; } + eccInit = 1; + if (wc_ecc_make_key_ex(&rng, (int)params.tradPrivSz, &eccKey, + params.eccCurveId) != 0) { result = -714; goto done; } +#else + result = -715; goto done; +#endif } - /* hasSignature=1 path: exercises sigSz accumulation */ + pubRaw = (byte*)WMALLOC(params.mldsaPubSz, NULL, 0); + if (pubRaw == NULL) { result = -716; goto done; } { - WMEMSET(&authData, 0, sizeof(authData)); - WMEMSET(&keySig, 0, sizeof(keySig)); - payloadSz = 0; - authData.sf.publicKey.privateKey = derKey; - authData.sf.publicKey.privateKeySz = (word32)derKeySz; - authData.sf.publicKey.hasSignature = 1; - keySig.keyId = keyId; - keySig.heap = NULL; - ret = wolfSSH_TestPrepareUserAuthRequestMlDsaCert(ssh, &payloadSz, - &authData, &keySig); - if (ret != WS_SUCCESS) { - wc_MlDsaKey_Free(&keySig.ks.mldsa.key); - result = -926; goto done; - } - if (keySig.sigSz == 0) { - wc_MlDsaKey_Free(&keySig.ks.mldsa.key); - result = -927; goto done; - } - if (payloadSz == 0) { - wc_MlDsaKey_Free(&keySig.ks.mldsa.key); - result = -928; goto done; - } - wc_MlDsaKey_Free(&keySig.ks.mldsa.key); + word32 sz = params.mldsaPubSz; + if (wc_MlDsaKey_ExportPubRaw(&signingKey, pubRaw, + &sz) != 0 || sz != params.mldsaPubSz) { result = -717; goto done; } + } + + tradPub = (byte*)WMALLOC(params.tradPubSz, NULL, 0); + if (tradPub == NULL) { result = -718; goto done; } + if (params.tradType == TRAD_TYPE_ED25519) { +#ifndef WOLFSSH_NO_ED25519 + word32 sz = params.tradPubSz; + if (wc_ed25519_export_public(&ed25519Key, tradPub, + &sz) != 0 || sz != params.tradPubSz) { result = -719; goto done; } +#endif } - -done: - WFREE(derKey, NULL, 0); + else if (params.tradType == TRAD_TYPE_ED448) { +#ifdef HAVE_ED448 + word32 sz = params.tradPubSz; + if (wc_ed448_export_public(&ed448Key, tradPub, + &sz) != 0 || sz != params.tradPubSz) { result = -720; goto done; } +#endif + } + else if (params.tradType == TRAD_TYPE_ECC) { +#ifndef WOLFSSH_NO_ECDSA + word32 sz = params.tradPubSz; + if (wc_ecc_export_x963(&eccKey, tradPub, + &sz) != 0 || sz != params.tradPubSz) { result = -721; goto done; } +#endif + } + + pubKeyBlobSz = UINT32_SZ * 2 + keyTypeNameSz + params.mldsaPubSz + + params.tradPubSz; + pubKeyBlob = (byte*)WMALLOC(pubKeyBlobSz, NULL, 0); + if (pubKeyBlob == NULL) { result = -722; goto done; } + + off = 0; + MlDsaTest_PutLen(pubKeyBlob + off, keyTypeNameSz); off += UINT32_SZ; + WMEMCPY(pubKeyBlob + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + MlDsaTest_PutLen(pubKeyBlob + off, + params.mldsaPubSz + params.tradPubSz); off += UINT32_SZ; + WMEMCPY(pubKeyBlob + off, pubRaw, + params.mldsaPubSz); off += params.mldsaPubSz; + WMEMCPY(pubKeyBlob + off, tradPub, + params.tradPubSz); off += params.tradPubSz; + + dataToSignSz = UINT32_SZ * 5 + usernameSz + serviceNameSz + + authNameSz + 1 + keyTypeNameSz + pubKeyBlobSz; + dataToSign = (byte*)WMALLOC(dataToSignSz, NULL, 0); + if (dataToSign == NULL) { result = -723; goto done; } + + off = 0; + MlDsaTest_PutLen(dataToSign + off, usernameSz); off += UINT32_SZ; + WMEMCPY(dataToSign + off, username, usernameSz); off += usernameSz; + MlDsaTest_PutLen(dataToSign + off, serviceNameSz); off += UINT32_SZ; + WMEMCPY(dataToSign + off, serviceName, serviceNameSz); off += serviceNameSz; + MlDsaTest_PutLen(dataToSign + off, authNameSz); off += UINT32_SZ; + WMEMCPY(dataToSign + off, authName, authNameSz); off += authNameSz; + dataToSign[off++] = 1; + MlDsaTest_PutLen(dataToSign + off, keyTypeNameSz); off += UINT32_SZ; + WMEMCPY(dataToSign + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + MlDsaTest_PutLen(dataToSign + off, pubKeyBlobSz); off += UINT32_SZ; + WMEMCPY(dataToSign + off, pubKeyBlob, pubKeyBlobSz); off += pubKeyBlobSz; + + checkDataSz = UINT32_SZ + ssh->sessionIdSz + MSG_ID_SZ + dataToSignSz; + checkData = (byte*)WMALLOC(checkDataSz, NULL, 0); + if (checkData == NULL) { result = -724; goto done; } + + off = 0; + MlDsaTest_PutLen(checkData + off, ssh->sessionIdSz); off += UINT32_SZ; + WMEMCPY(checkData + off, ssh->sessionId, + ssh->sessionIdSz); off += ssh->sessionIdSz; + checkData[off++] = MSGID_USERAUTH_REQUEST; + WMEMCPY(checkData + off, dataToSign, dataToSignSz); + + mldsaSig = (byte*)WMALLOC(params.mldsaSigSz, NULL, 0); + tradSig = (byte*)WMALLOC(params.tradSigSz + 256, NULL, 0); + mldsaSigSz = params.mldsaSigSz; + + if (mldsaSig == NULL || tradSig == NULL) { result = -725; goto done; } + + hash = (byte*)WMALLOC(params.tradHashSz, NULL, 0); + m_prime_len = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + + params.tradHashSz; + m_prime = (byte*)WMALLOC(m_prime_len, NULL, 0); + + if (hash == NULL || m_prime == NULL) { result = -726; goto done; } + + ret = WS_Hash_Helper(params.tradHashId, checkData, checkDataSz, hash, + params.tradHashSz); + if (ret != 0) { result = -727; goto done; } + + WMEMCPY(m_prime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + WMEMCPY(m_prime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); + m_prime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; + WMEMCPY(m_prime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, + params.tradHashSz); + + if (wc_MlDsaKey_SignCtx(&signingKey, (const byte*)params.label, + params.labelSz, mldsaSig, &mldsaSigSz, m_prime, m_prime_len, + &rng) != 0) { + result = -728; goto done; + } + + if (params.tradType == TRAD_TYPE_ED25519) { +#ifndef WOLFSSH_NO_ED25519 + tradSigSz = ED25519_SIG_SIZE; + if (wc_ed25519_sign_msg(m_prime, m_prime_len, tradSig, &tradSigSz, + &ed25519Key) != 0) { result = -729; goto done; } +#endif + } + else if (params.tradType == TRAD_TYPE_ED448) { +#ifdef HAVE_ED448 + tradSigSz = 114; + if (wc_ed448_sign_msg(m_prime, m_prime_len, tradSig, &tradSigSz, + &ed448Key, NULL, 0) != 0) { result = -730; goto done; } +#endif + } + else if (params.tradType == TRAD_TYPE_ECC) { +#ifndef WOLFSSH_NO_ECDSA + byte asnSig[ECDSA_ASN_SIG_SZ]; + word32 asnSigSz = sizeof(asnSig); + byte digest[WC_MAX_DIGEST_SIZE]; + word32 rSz = MAX_ECC_BYTES + ECC_MAX_PAD_SZ, + sSz = MAX_ECC_BYTES + ECC_MAX_PAD_SZ; + byte rBuf[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; + byte sBuf[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; + + ret = WS_Hash_Helper(params.tradHashId, m_prime, m_prime_len, digest, + params.tradHashSz); + if (ret != 0) { result = -737; goto done; } + + if (wc_ecc_sign_hash(digest, params.tradHashSz, asnSig, &asnSigSz, + &rng, &eccKey) != 0) { result = -731; goto done; } + + if (wc_ecc_sig_to_rs(asnSig, asnSigSz, rBuf, &rSz, sBuf, + &sSz) != 0) { result = -734; goto done; } + { + /* mpint pad, mirroring CompositeEccSign()'s wire format. */ + byte rPad = (rBuf[0] & 0x80) ? 1 : 0; + byte sPad = (sBuf[0] & 0x80) ? 1 : 0; + + off = 0; + MlDsaTest_PutLen(tradSig + off, rSz + rPad); off += UINT32_SZ; + if (rPad) + tradSig[off++] = 0; + WMEMCPY(tradSig + off, rBuf, rSz); off += rSz; + MlDsaTest_PutLen(tradSig + off, sSz + sPad); off += UINT32_SZ; + if (sPad) + tradSig[off++] = 0; + WMEMCPY(tradSig + off, sBuf, sSz); off += sSz; + tradSigSz = off; + } +#endif + } + + /* extra padding so tamperSig==4's oversized claimed length reads + * initialized (zeroed) memory instead of running off the allocation */ + sigBlobSz = UINT32_SZ * 2 + keyTypeNameSz + mldsaSigSz + tradSigSz + 64; + sigBlob = (byte*)WMALLOC(sigBlobSz, NULL, 0); + if (sigBlob == NULL) { result = -735; goto done; } + WMEMSET(sigBlob, 0, sigBlobSz); + + off = 0; + MlDsaTest_PutLen(sigBlob + off, keyTypeNameSz); off += UINT32_SZ; + WMEMCPY(sigBlob + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + { + word32 sigLenOff = off; + MlDsaTest_PutLen(sigBlob + off, mldsaSigSz + tradSigSz); + off += UINT32_SZ; + WMEMCPY(sigBlob + off, mldsaSig, mldsaSigSz); off += mldsaSigSz; + WMEMCPY(sigBlob + off, tradSig, tradSigSz); off += tradSigSz; + + if (tamperSig == 3) { + /* Claim a composite signature shorter than the ML-DSA half + * alone; must be rejected by VerifyMlDsaComposite()'s + * "sigSz < params.mldsaSigSz" underflow guard. */ + MlDsaTest_PutLen(sigBlob + sigLenOff, mldsaSigSz - 1); + } + else if (tamperSig == 4) { + /* Claim a composite signature far longer than what was + * actually produced; must be rejected by + * VerifyMlDsaComposite()'s trad-region bound checks rather + * than reading past the real signature data. */ + MlDsaTest_PutLen(sigBlob + sigLenOff, + mldsaSigSz + tradSigSz + 32); + } + } + + if (tamperSig == 1) { + /* Flip a byte in the trad-algorithm component of the signature + * (last byte of the real content, before the padding added for + * tamperSig==4) and confirm verification rejects it rather than + * accepting a corrupted composite signature. */ + sigBlob[off - 1] ^= 0xFF; + } + else if (tamperSig == 2) { + /* Flip the first byte of the ML-DSA component of the signature + * and confirm verification rejects it too; a bug that verifies + * only the trad half (or mis-slices the combined signature) + * would let this slip through. */ + word32 mldsaOff = UINT32_SZ * 2 + keyTypeNameSz; + sigBlob[mldsaOff] ^= 0xFF; + } + + WMEMSET(&authData, 0, sizeof(authData)); + authData.type = WOLFSSH_USERAUTH_PUBLICKEY; + authData.username = (const byte*)username; + authData.usernameSz = usernameSz; + authData.serviceName = (const byte*)serviceName; + authData.serviceNameSz = serviceNameSz; + authData.authName = (const byte*)authName; + authData.authNameSz = authNameSz; + authData.sf.publicKey.dataToSign = dataToSign; + authData.sf.publicKey.publicKeyType = (const byte*)keyTypeName; + authData.sf.publicKey.publicKeyTypeSz = keyTypeNameSz; + authData.sf.publicKey.publicKey = pubKeyBlob; + authData.sf.publicKey.publicKeySz = pubKeyBlobSz; + authData.sf.publicKey.signature = sigBlob; + authData.sf.publicKey.signatureSz = off; + + ret = wolfSSH_TestDoUserAuthRequestMlDsaComposite(ssh, &authData, keyId, + pubKeyBlobSz); + if (tamperSig) { + if (ret == WS_SUCCESS) { + printf("DoUserAuthRequestMlDsaComposite (%s) tampered sig " + "incorrectly verified as valid\n", keyTypeName); + result = -738; + } + } + else if (ret != WS_SUCCESS) { + printf("DoUserAuthRequestMlDsaComposite (%s) failed: ret=%d\n", + keyTypeName, ret); + result = -736; + } + +done: + if (signingKeyInit) wc_MlDsaKey_Free(&signingKey); +#ifndef WOLFSSH_NO_ECDSA + if (eccInit) wc_ecc_free(&eccKey); +#endif +#ifndef WOLFSSH_NO_ED25519 + if (ed25519Init) wc_ed25519_free(&ed25519Key); +#endif +#ifdef HAVE_ED448 + if (ed448Init) wc_ed448_free(&ed448Key); +#endif + if (rngInit) wc_FreeRng(&rng); + if (pubKeyBlob != NULL) WFREE(pubKeyBlob, NULL, 0); + if (sigBlob != NULL) WFREE(sigBlob, NULL, 0); + if (dataToSign != NULL) WFREE(dataToSign, NULL, 0); + if (checkData != NULL) WFREE(checkData, NULL, 0); + if (pubRaw != NULL) WFREE(pubRaw, NULL, 0); + if (tradPub != NULL) WFREE(tradPub, NULL, 0); + if (mldsaSig != NULL) WFREE(mldsaSig, NULL, 0); + if (tradSig != NULL) WFREE(tradSig, NULL, 0); + if (hash != NULL) WFREE(hash, NULL, 0); + if (m_prime != NULL) WFREE(m_prime, NULL, 0); + if (ssh != NULL) wolfSSH_free(ssh); + if (ctx != NULL) wolfSSH_CTX_free(ctx); + return result; +} + +/* Exercises SignHMlDsaComposite(), the KEX host-key signing path used by + * SendKexDhReply(). Confirms: (1) signing succeeds and produces exactly + * mldsaSigSz + tradSigSz bytes when the caller's buffer is exactly that + * size (matching production usage via KEX_SIG_SIZE), and (2) a buffer + * that is one byte too small -- including the historical 64-byte trad + * headroom that overflowed for ECC/Ed448 composites before the fix in + * WS_GetCompositeParams()/SignHMlDsaComposite() -- is safely rejected with + * WS_BAD_ARGUMENT rather than overflowing. */ +static int test_SignHMlDsaComposite_Params(const char* label, byte keyId) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + CompositeParams params; + byte sig[WC_MLDSA_87_SIG_SIZE + 256]; + word32 sigSz; + int ret, result = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) return -900; + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -901; goto done; } + + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xA5, ssh->hSz); + + if (WS_GetCompositeParams(keyId, ¶ms) != WS_SUCCESS) { + result = -902; goto done; + } + + /* Buffer sized to the worst case: must succeed. ED25519/ED448 trad + * signatures are fixed-length, so sigSz must match exactly for those. + * ECC r/s encoding is variable length (a leading-zero pad byte may or + * may not be needed per coordinate), so allow up to 2 bytes of slack + * per coordinate (4 bytes total) for ECC composites only -- anything + * looser than that would fail to catch a severely truncated or + * dropped trad signature. */ + { + word32 minSigSz = params.mldsaSigSz + params.tradSigSz; + if (params.tradType == TRAD_TYPE_ECC) { + minSigSz -= (minSigSz > 4) ? 4 : minSigSz; + } + sigSz = params.mldsaSigSz + params.tradSigSz; + ret = wolfSSH_TestSignHMlDsaComposite(ssh, sig, &sigSz, keyId); + if (ret != WS_SUCCESS || sigSz < minSigSz || + sigSz > (params.mldsaSigSz + params.tradSigSz)) { + printf("SignHMlDsaComposite (%s) worst-case-size sign failed: " + "ret=%d sigSz=%u min=%u max=%u\n", label, ret, sigSz, + minSigSz, params.mldsaSigSz + params.tradSigSz); + result = -903; goto done; + } + } + + /* No room at all for the trad component: must be rejected, not + * overflow the caller's buffer. (Not testing "one byte short of the + * worst case" here -- ECC r/s encoding is variable length, so a + * real signature can legitimately land a byte or two under the + * worst-case reserved size, which would make that boundary flaky.) */ + sigSz = params.mldsaSigSz + 1; + ret = wolfSSH_TestSignHMlDsaComposite(ssh, sig, &sigSz, keyId); + if (ret != WS_BAD_ARGUMENT) { + printf("SignHMlDsaComposite (%s) undersized buffer not rejected: " + "ret=%d\n", label, ret); + result = -904; goto done; + } + + /* Historical KEX_SIG_SIZE reserved only 64 bytes of trad headroom, + * too small for P-384/Ed448. Exercises the same generic tradSigSz + * check as above, not the KEX_SIG_SIZE constant itself. */ + if (params.tradSigSz > 64) { + sigSz = params.mldsaSigSz + 64; + ret = wolfSSH_TestSignHMlDsaComposite(ssh, sig, &sigSz, keyId); + if (ret != WS_BAD_ARGUMENT) { + printf("SignHMlDsaComposite (%s) 64-byte trad headroom not " + "rejected: ret=%d\n", label, ret); + result = -905; goto done; + } + } + +done: + if (ssh != NULL) wolfSSH_free(ssh); + if (ctx != NULL) wolfSSH_CTX_free(ctx); + return result; +} + +static int test_SignHMlDsaComposite(void) +{ + int ret = 0; +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa44-es256@wolfssl.com", + ID_MLDSA44_ES256); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa65-es256@wolfssl.com", + ID_MLDSA65_ES256); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa87-es384@wolfssl.com", + ID_MLDSA87_ES384); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa44-ed25519@openssh.com", + ID_MLDSA44_ED25519); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa65-ed25519@wolfssl.com", + ID_MLDSA65_ED25519); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa87-ed448@wolfssl.com", + ID_MLDSA87_ED448); + if (ret != 0) return ret; +#endif + return 0; +} + +/* Builds a minimal OpenSSH-key-v1 envelope and drives it through the real + * envelope parser (GetOpenSshKey() -> GetOpenSshKeyMlDsaComposite()), rather + * than calling GetOpenSshKeyMlDsaComposite() directly on synthetic keys -- + * that gap is what let "wrong parser used on the raw envelope" bugs ship + * undetected in earlier revisions. */ +static int test_PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope(void) +{ +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + static const char keyTypeName[] = "ssh-mldsa44-ed25519@openssh.com"; + static const char magic[] = "openssh-key-v1"; + static const char none[] = "none"; + static const char comment[] = ""; + const word32 keyTypeNameSz = (word32)(sizeof(keyTypeName) - 1); + const word32 noneSz = (word32)(sizeof(none) - 1); + const word32 commentSz = (word32)(sizeof(comment) - 1); + + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WC_RNG rng; + int rngInit = 0; + CompositeParams params; + WS_KeySignature keySig; + WS_UserAuthData authData; + MlDsaKey signingKey; + int signingKeyInit = 0; + ed25519_key ed25519Key; + int ed25519Init = 0; + int result = 0; + int ret; + + byte mldsaSeed[MLDSA_SEED_SZ]; + byte ed25519Seed[ED25519_KEY_SIZE]; + byte* mldsaPub = NULL; + byte* ed25519Pub = NULL; + word32 mldsaPubSz = 0; + word32 ed25519PubSz = ED25519_PUB_KEY_SIZE; + byte roundTripPub[WC_MLDSA_44_PUB_KEY_SIZE]; + word32 roundTripPubSz; + + byte* file = NULL; + word32 fileSz; + word32 off; + word32 pubBlobSz, compositePubSz, compositePrivSz, privKeysStrSz, padSz, i; + word32 compPubOff1 = 0, compPrivOff = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) return -950; + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -951; goto done; } + + if (wc_InitRng(&rng) != 0) { result = -952; goto done; } + rngInit = 1; + + ret = WS_GetCompositeParams(ID_MLDSA44_ED25519, ¶ms); + if (ret != WS_SUCCESS) { result = -953; goto done; } + + /* Generate real seed-based keys, per the composite draft's + * seed-based KeyGen_internal requirement (draft section 4.1). */ + if (wc_RNG_GenerateBlock(&rng, mldsaSeed, sizeof(mldsaSeed)) != 0) { + result = -954; goto done; + } + if (wc_MlDsaKey_Init(&signingKey, NULL, + INVALID_DEVID) != 0) { result = -955; goto done; } + signingKeyInit = 1; + if (wc_MlDsaKey_SetParams(&signingKey, + params.mldsaLevel) != 0) { result = -956; goto done; } + if (wc_MlDsaKey_MakeKeyFromSeed(&signingKey, + mldsaSeed) != 0) { result = -957; goto done; } + + mldsaPub = (byte*)WMALLOC(params.mldsaPubSz, NULL, 0); + if (mldsaPub == NULL) { result = -958; goto done; } + mldsaPubSz = params.mldsaPubSz; + if (wc_MlDsaKey_ExportPubRaw(&signingKey, mldsaPub, &mldsaPubSz) != 0 + || mldsaPubSz != params.mldsaPubSz) { + result = -959; goto done; + } + + if (wc_RNG_GenerateBlock(&rng, ed25519Seed, sizeof(ed25519Seed)) != 0) { + result = -960; goto done; + } + if (wc_ed25519_init(&ed25519Key) != 0) { result = -961; goto done; } + ed25519Init = 1; + if (wc_ed25519_import_private_only(ed25519Seed, sizeof(ed25519Seed), + &ed25519Key) != 0) { + result = -962; goto done; + } + ed25519Pub = (byte*)WMALLOC(ED25519_PUB_KEY_SIZE, NULL, 0); + if (ed25519Pub == NULL) { result = -963; goto done; } + if (wc_ed25519_make_public(&ed25519Key, ed25519Pub, ed25519PubSz) != 0) { + result = -964; goto done; + } + /* wc_ed25519_make_public() alone doesn't mark the key as having a + * public part for later use; import the full pair explicitly. */ + wc_ed25519_free(&ed25519Key); + ed25519Init = 0; + if (wc_ed25519_init(&ed25519Key) != 0) { result = -965; goto done; } + ed25519Init = 1; + if (wc_ed25519_import_private_key(ed25519Seed, sizeof(ed25519Seed), + ed25519Pub, ed25519PubSz, &ed25519Key) != 0) { + result = -966; goto done; + } + + /* Build the OpenSSH-key-v1 envelope: + * magic "openssh-key-v1\0", string ciphername, string kdfname, + * string kdfoptions, uint32 keycount, string pubkeyblob, + * string { checkint1, checkint2, { string type, string pub, + * string priv, string comment } x keycount, padding } */ + compositePubSz = params.mldsaPubSz + params.tradPubSz; + compositePrivSz = MLDSA_SEED_SZ + ED25519_KEY_SIZE; + + pubBlobSz = UINT32_SZ + keyTypeNameSz + UINT32_SZ + compositePubSz; + privKeysStrSz = UINT32_SZ * 2 /* checkints */ + + UINT32_SZ + keyTypeNameSz + + UINT32_SZ + compositePubSz + + UINT32_SZ + compositePrivSz + + UINT32_SZ + commentSz; + padSz = (MIN_BLOCK_SZ - (privKeysStrSz % MIN_BLOCK_SZ)) % MIN_BLOCK_SZ; + privKeysStrSz += padSz; + + fileSz = (word32)WSTRLEN(magic) + 1 + + UINT32_SZ + noneSz /* ciphername */ + + UINT32_SZ + noneSz /* kdfname */ + + UINT32_SZ /* kdfoptions (empty) */ + + UINT32_SZ /* keycount */ + + UINT32_SZ + pubBlobSz + + UINT32_SZ + privKeysStrSz; + + file = (byte*)WMALLOC(fileSz, NULL, 0); + if (file == NULL) { result = -967; goto done; } + + off = 0; + WMEMCPY(file + off, magic, WSTRLEN(magic) + 1); off += (word32)WSTRLEN( + magic) + 1; + MlDsaTest_PutLen(file + off, noneSz); off += UINT32_SZ; + WMEMCPY(file + off, none, noneSz); off += noneSz; + MlDsaTest_PutLen(file + off, noneSz); off += UINT32_SZ; + WMEMCPY(file + off, none, noneSz); off += noneSz; + MlDsaTest_PutLen(file + off, 0); off += UINT32_SZ; /* kdfoptions */ + MlDsaTest_PutLen(file + off, 1); off += UINT32_SZ; /* keycount */ + + MlDsaTest_PutLen(file + off, pubBlobSz); off += UINT32_SZ; + MlDsaTest_PutLen(file + off, keyTypeNameSz); off += UINT32_SZ; + WMEMCPY(file + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + compPubOff1 = off; + MlDsaTest_PutLen(file + off, compositePubSz); off += UINT32_SZ; + WMEMCPY(file + off, mldsaPub, mldsaPubSz); off += mldsaPubSz; + WMEMCPY(file + off, ed25519Pub, ed25519PubSz); off += ed25519PubSz; + + MlDsaTest_PutLen(file + off, privKeysStrSz); off += UINT32_SZ; + MlDsaTest_PutLen(file + off, 0xC0FFEEEE); off += UINT32_SZ; /* checkint1 */ + MlDsaTest_PutLen(file + off, 0xC0FFEEEE); off += UINT32_SZ; /* checkint2 */ + MlDsaTest_PutLen(file + off, keyTypeNameSz); off += UINT32_SZ; + WMEMCPY(file + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + /* compPubOff2 = off; (unused) */ + MlDsaTest_PutLen(file + off, compositePubSz); off += UINT32_SZ; + WMEMCPY(file + off, mldsaPub, mldsaPubSz); off += mldsaPubSz; + WMEMCPY(file + off, ed25519Pub, ed25519PubSz); off += ed25519PubSz; + compPrivOff = off; + MlDsaTest_PutLen(file + off, compositePrivSz); off += UINT32_SZ; + WMEMCPY(file + off, mldsaSeed, MLDSA_SEED_SZ); off += MLDSA_SEED_SZ; + WMEMCPY(file + off, ed25519Seed, ED25519_KEY_SIZE); off += ED25519_KEY_SIZE; + MlDsaTest_PutLen(file + off, commentSz); off += UINT32_SZ; + for (i = 1; i <= padSz; i++) { + file[off++] = (byte)i; + } + + if (off != fileSz) { result = -968; goto done; } + + WMEMSET(&keySig, 0, sizeof(keySig)); + keySig.keyId = ID_MLDSA44_ED25519; + keySig.heap = NULL; + + WMEMSET(&authData, 0, sizeof(authData)); + authData.sf.publicKey.privateKey = file; + authData.sf.publicKey.privateKeySz = fileSz; + authData.sf.publicKey.hasSignature = 0; + + { + word32 payloadSz = 0; + ret = wolfSSH_TestPrepareUserAuthRequestMlDsaComposite(ssh, &payloadSz, + &authData, &keySig); + } + if (ret != WS_SUCCESS) { + printf("PrepareUserAuthRequestMlDsaComposite OpenSSH envelope " + "decode failed: ret=%d\n", ret); + result = -969; goto done; + } + + /* Confirm the decoded key is the one that was actually encoded, not + * just that decoding didn't crash. */ + roundTripPubSz = sizeof(roundTripPub); + if (wc_MlDsaKey_ExportPubRaw(&keySig.ks.mldsa_composite.mldsa, + roundTripPub, &roundTripPubSz) != 0 + || roundTripPubSz != mldsaPubSz + || WMEMCMP(roundTripPub, mldsaPub, mldsaPubSz) != 0) { + printf("PrepareUserAuthRequestMlDsaComposite OpenSSH envelope: " + "ML-DSA public key round-trip mismatch\n"); + result = -970; + } + wolfSSH_KEY_clean(&keySig); + + /* Negative test: shrinking compositePubSz by 1 in the public section */ + MlDsaTest_PutLen(file + compPubOff1, compositePubSz - 1); + { + word32 payloadSz = 0; + ret = wolfSSH_TestPrepareUserAuthRequestMlDsaComposite(ssh, &payloadSz, + &authData, &keySig); + if (ret >= 0) { + printf("PrepareUserAuthRequestMlDsaComposite: expected failure for shrunken pub blob, got %d\n", ret); + result = -971; + wolfSSH_KEY_clean(&keySig); + } + } + MlDsaTest_PutLen(file + compPubOff1, compositePubSz); /* restore */ + + /* Negative test: extending compositePrivSz by 1 in the private section */ + MlDsaTest_PutLen(file + compPrivOff, compositePrivSz + 1); + { + word32 payloadSz = 0; + ret = wolfSSH_TestPrepareUserAuthRequestMlDsaComposite(ssh, &payloadSz, + &authData, &keySig); + if (ret >= 0) { + printf("PrepareUserAuthRequestMlDsaComposite: expected failure for enlarged priv blob, got %d\n", ret); + result = -972; + wolfSSH_KEY_clean(&keySig); + } + } + MlDsaTest_PutLen(file + compPrivOff, compositePrivSz); /* restore */ + + +done: + if (signingKeyInit) wc_MlDsaKey_Free(&signingKey); + if (ed25519Init) wc_ed25519_free(&ed25519Key); + if (rngInit) wc_FreeRng(&rng); + if (mldsaPub != NULL) WFREE(mldsaPub, NULL, 0); + if (ed25519Pub != NULL) WFREE(ed25519Pub, NULL, 0); + if (file != NULL) WFREE(file, NULL, 0); + if (ssh != NULL) wolfSSH_free(ssh); + if (ctx != NULL) wolfSSH_CTX_free(ctx); + return result; +#else + return 0; +#endif +} + +/* Drives BuildUserAuthRequestMlDsaComposite() -- the client-side composite + * signer, otherwise untested -- and feeds its output straight into + * DoUserAuthRequestMlDsaComposite() (exercised above) to confirm the two + * agree on the wire format; this is the path a real hybrid client uses + * to authenticate. */ +#ifdef WOLFSSH_KEYGEN +static int test_BuildUserAuthRequestMlDsaComposite_Params( + const char* keyTypeName, word32 level, word32 tradType, byte keyId) +{ + static const char username[] = "wolfssh"; + static const char serviceName[] = "ssh-connection"; + static const char authName[] = "publickey"; + const word32 keyTypeNameSz = (word32)(WSTRLEN(keyTypeName)); + const word32 usernameSz = (word32)(sizeof(username) - 1); + const word32 serviceNameSz = (word32)(sizeof(serviceName) - 1); + const word32 authNameSz = (word32)(sizeof(authName) - 1); + + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + CompositeParams params; + WS_KeySignature keySig; + WS_UserAuthData authData; + const CompositeTradOps* ops; + int prepared = 0; + + byte* privKey = NULL; + word32 privKeySz; + byte* mldsaPub = NULL; + byte* tradPub = NULL; + byte* pubKeyBlob = NULL; + byte* packet = NULL; + word32 pubKeyBlobSz = 0; + word32 dataToSignSz = 0; + word32 packetSz; + word32 packetBufSz; + word32 off; + word32 payloadSz = 0; + word32 idx; + int result = 0; + int ret; + + WMEMSET(&keySig, 0, sizeof(keySig)); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) return -980; + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -981; goto done; } + + ssh->sessionIdSz = 16; + WMEMSET(ssh->sessionId, 0xA5, ssh->sessionIdSz); + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) { result = -982; goto done; } + + privKey = (byte*)WMALLOC(8192, NULL, 0); + if (privKey == NULL) { result = -983; goto done; } + ret = wolfSSH_MakeMlDsaCompositeKey(privKey, 8192, level, tradType); + if (ret < 0) { result = -984; goto done; } + privKeySz = (word32)ret; + + { + byte* decodedPrivKey = NULL; + word32 decodedPrivKeySz = 0; + const byte* outType = NULL; + word32 outTypeSz = 0; + ret = wolfSSH_ReadKey_buffer(privKey, privKeySz, WOLFSSH_FORMAT_OPENSSH, + &decodedPrivKey, &decodedPrivKeySz, &outType, &outTypeSz, NULL); + if (ret != WS_SUCCESS) { + printf("BuildUserAuthRequestMlDsaComposite (%s): " + "ReadKey_buffer failed ret=%d\n", keyTypeName, ret); + result = -984; goto done; + } + WMEMCPY(privKey, decodedPrivKey, decodedPrivKeySz); + privKeySz = decodedPrivKeySz; + WFREE(decodedPrivKey, NULL, DYNTYPE_PRIVKEY); + } + + keySig.keyId = keyId; + keySig.heap = NULL; + + WMEMSET(&authData, 0, sizeof(authData)); + authData.sf.publicKey.privateKey = privKey; + authData.sf.publicKey.privateKeySz = privKeySz; + authData.sf.publicKey.hasSignature = 1; + authData.sf.publicKey.publicKeyType = (const byte*)keyTypeName; + authData.sf.publicKey.publicKeyTypeSz = keyTypeNameSz; + + ret = wolfSSH_TestPrepareUserAuthRequestMlDsaComposite(ssh, &payloadSz, + &authData, &keySig); + if (ret != WS_SUCCESS) { + printf("BuildUserAuthRequestMlDsaComposite (%s): prepare failed " + "ret=%d\n", keyTypeName, ret); + result = -985; goto done; + } + prepared = 1; + + mldsaPub = (byte*)WMALLOC(params.mldsaPubSz, NULL, 0); + tradPub = (byte*)WMALLOC(params.tradPubSz, NULL, 0); + if (mldsaPub == NULL || tradPub == NULL) { result = -986; goto done; } + + { + word32 sz = params.mldsaPubSz; + if (wc_MlDsaKey_ExportPubRaw(&keySig.ks.mldsa_composite.mldsa, + mldsaPub, &sz) != 0 || sz != params.mldsaPubSz) { + result = -987; goto done; + } + } + + ops = WS_GetTradOps(params.tradType); + if (ops == NULL) { result = -988; goto done; } + { + word32 sz = params.tradPubSz; + if (ops->exportPub(&keySig.ks.mldsa_composite.trad, tradPub, &sz) + != 0 || sz != params.tradPubSz) { + result = -989; goto done; + } + } + + pubKeyBlobSz = UINT32_SZ * 2 + keyTypeNameSz + + params.mldsaPubSz + params.tradPubSz; + pubKeyBlob = (byte*)WMALLOC(pubKeyBlobSz, NULL, 0); + if (pubKeyBlob == NULL) { result = -990; goto done; } + + off = 0; + MlDsaTest_PutLen(pubKeyBlob + off, keyTypeNameSz); off += UINT32_SZ; + WMEMCPY(pubKeyBlob + off, keyTypeName, keyTypeNameSz); + off += keyTypeNameSz; + word32 pubBlobTagOff = off; + MlDsaTest_PutLen(pubKeyBlob + off, params.mldsaPubSz + params.tradPubSz); + off += UINT32_SZ; + WMEMCPY(pubKeyBlob + off, mldsaPub, params.mldsaPubSz); + off += params.mldsaPubSz; + WMEMCPY(pubKeyBlob + off, tradPub, params.tradPubSz); + off += params.tradPubSz; + + dataToSignSz = UINT32_SZ * 5 + usernameSz + serviceNameSz + authNameSz + + 1 + keyTypeNameSz + pubKeyBlobSz; + packetSz = MSG_ID_SZ + dataToSignSz; + packetBufSz = packetSz + UINT32_SZ * 2 + keyTypeNameSz + keySig.sigSz + + 32; + packet = (byte*)WMALLOC(packetBufSz, NULL, 0); + if (packet == NULL) { result = -991; goto done; } + + off = 0; + packet[off++] = MSGID_USERAUTH_REQUEST; + MlDsaTest_PutLen(packet + off, usernameSz); off += UINT32_SZ; + WMEMCPY(packet + off, username, usernameSz); off += usernameSz; + MlDsaTest_PutLen(packet + off, serviceNameSz); off += UINT32_SZ; + WMEMCPY(packet + off, serviceName, serviceNameSz); off += serviceNameSz; + MlDsaTest_PutLen(packet + off, authNameSz); off += UINT32_SZ; + WMEMCPY(packet + off, authName, authNameSz); off += authNameSz; + packet[off++] = 1; + MlDsaTest_PutLen(packet + off, keyTypeNameSz); off += UINT32_SZ; + WMEMCPY(packet + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + MlDsaTest_PutLen(packet + off, pubKeyBlobSz); off += UINT32_SZ; + WMEMCPY(packet + off, pubKeyBlob, pubKeyBlobSz); off += pubKeyBlobSz; + + if (off != packetSz) { result = -992; goto done; } + + idx = packetSz; + ret = wolfSSH_TestBuildUserAuthRequestMlDsaComposite(ssh, packet, &idx, + &authData, packet, 0, &keySig); + if (ret != WS_SUCCESS) { + printf("BuildUserAuthRequestMlDsaComposite (%s): build failed " + "ret=%d\n", keyTypeName, ret); + result = -993; goto done; + } + + /* Build appends string(string(type) + string(sig)); Do expects + * pk->signature to be the inner content, without that outer length + * prefix. */ + authData.username = (const byte*)username; + authData.usernameSz = usernameSz; + authData.serviceName = (const byte*)serviceName; + authData.serviceNameSz = serviceNameSz; + authData.authName = (const byte*)authName; + authData.authNameSz = authNameSz; + authData.sf.publicKey.dataToSign = packet + MSG_ID_SZ; + authData.sf.publicKey.publicKey = pubKeyBlob; + authData.sf.publicKey.publicKeySz = pubKeyBlobSz; + authData.sf.publicKey.signature = packet + packetSz + LENGTH_SZ; + authData.sf.publicKey.signatureSz = idx - packetSz - LENGTH_SZ; + + ret = wolfSSH_TestDoUserAuthRequestMlDsaComposite(ssh, &authData, keyId, + pubKeyBlobSz); + if (ret != WS_SUCCESS) { + printf("BuildUserAuthRequestMlDsaComposite (%s): verify of built " + "signature failed ret=%d\n", keyTypeName, ret); + result = -994; + } + + /* Negative test: shrink composite pub key length tag */ + MlDsaTest_PutLen(pubKeyBlob + pubBlobTagOff, params.mldsaPubSz + params.tradPubSz - 1); + ret = wolfSSH_TestDoUserAuthRequestMlDsaComposite(ssh, &authData, keyId, + pubKeyBlobSz); + if (ret >= 0) { + printf("BuildUserAuthRequestMlDsaComposite (%s): verify of shrunken " + "blob succeeded\n", keyTypeName); + result = -995; + } + MlDsaTest_PutLen(pubKeyBlob + pubBlobTagOff, params.mldsaPubSz + params.tradPubSz); + +done: + if (prepared) { + wolfSSH_KEY_clean(&keySig); + } + if (privKey != NULL) WFREE(privKey, NULL, 0); + if (mldsaPub != NULL) WFREE(mldsaPub, NULL, 0); + if (tradPub != NULL) WFREE(tradPub, NULL, 0); + if (pubKeyBlob != NULL) WFREE(pubKeyBlob, NULL, 0); + if (packet != NULL) WFREE(packet, NULL, 0); + if (ssh != NULL) wolfSSH_free(ssh); + if (ctx != NULL) wolfSSH_CTX_free(ctx); + return result; +} + +static int test_BuildUserAuthRequestMlDsaComposite(void) +{ + int ret = 0; +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-es256@wolfssl.com", WOLFSSH_MLDSAKEY_44, + WOLFSSH_COMPOSITE_TRAD_ECDSA, ID_MLDSA44_ES256); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa65-es256@wolfssl.com", WOLFSSH_MLDSAKEY_65, + WOLFSSH_COMPOSITE_TRAD_ECDSA, ID_MLDSA65_ES256); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa87-es384@wolfssl.com", WOLFSSH_MLDSAKEY_87, + WOLFSSH_COMPOSITE_TRAD_ECDSA, ID_MLDSA87_ES384); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", WOLFSSH_MLDSAKEY_44, + WOLFSSH_COMPOSITE_TRAD_ED25519, ID_MLDSA44_ED25519); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa65-ed25519@wolfssl.com", WOLFSSH_MLDSAKEY_65, + WOLFSSH_COMPOSITE_TRAD_ED25519, ID_MLDSA65_ED25519); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa87-ed448@wolfssl.com", WOLFSSH_MLDSAKEY_87, + WOLFSSH_COMPOSITE_TRAD_ED448, ID_MLDSA87_ED448); + if (ret != 0) return ret; +#endif + return 0; +} +#endif /* WOLFSSH_KEYGEN */ + +static int test_DoUserAuthRequestMlDsaComposite(void) +{ + int ret = 0; +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256@wolfssl.com", + ID_MLDSA44_ES256, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256@wolfssl.com", + ID_MLDSA44_ES256, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256@wolfssl.com", + ID_MLDSA44_ES256, 2); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256@wolfssl.com", + ID_MLDSA44_ES256, 3); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256@wolfssl.com", + ID_MLDSA44_ES256, 4); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256@wolfssl.com", + ID_MLDSA65_ES256, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256@wolfssl.com", + ID_MLDSA65_ES256, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256@wolfssl.com", + ID_MLDSA65_ES256, 2); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256@wolfssl.com", + ID_MLDSA65_ES256, 3); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256@wolfssl.com", + ID_MLDSA65_ES256, 4); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384@wolfssl.com", + ID_MLDSA87_ES384, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384@wolfssl.com", + ID_MLDSA87_ES384, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384@wolfssl.com", + ID_MLDSA87_ES384, 2); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384@wolfssl.com", + ID_MLDSA87_ES384, 3); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384@wolfssl.com", + ID_MLDSA87_ES384, 4); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 2); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 3); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 4); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519@wolfssl.com", + ID_MLDSA65_ED25519, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519@wolfssl.com", + ID_MLDSA65_ED25519, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519@wolfssl.com", + ID_MLDSA65_ED25519, 2); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519@wolfssl.com", + ID_MLDSA65_ED25519, 3); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519@wolfssl.com", + ID_MLDSA65_ED25519, 4); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448@wolfssl.com", + ID_MLDSA87_ED448, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448@wolfssl.com", + ID_MLDSA87_ED448, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448@wolfssl.com", + ID_MLDSA87_ED448, 2); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448@wolfssl.com", + ID_MLDSA87_ED448, 3); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448@wolfssl.com", + ID_MLDSA87_ED448, 4); + if (ret != 0) return ret; +#endif + return 0; +} + +#ifdef WOLFSSH_KEYGEN +static int test_PrepareUserAuthRequestMlDsa(void) +{ + int ret = 0; +#ifndef WOLFSSH_NO_MLDSA44 + ret = test_PrepareUserAuthRequestMlDsa_Params(WOLFSSH_MLDSAKEY_44, + ID_MLDSA44, WC_MLDSA_44_BOTH_KEY_DER_SIZE); + if (ret != 0) return ret; +#endif +#ifndef WOLFSSH_NO_MLDSA65 + ret = test_PrepareUserAuthRequestMlDsa_Params(WOLFSSH_MLDSAKEY_65, + ID_MLDSA65, WC_MLDSA_65_BOTH_KEY_DER_SIZE); + if (ret != 0) return ret; +#endif +#ifndef WOLFSSH_NO_MLDSA87 + ret = test_PrepareUserAuthRequestMlDsa_Params(WOLFSSH_MLDSAKEY_87, + ID_MLDSA87, WC_MLDSA_87_BOTH_KEY_DER_SIZE); + if (ret != 0) return ret; +#endif + (void)ret; + return 0; +} + +#ifdef WOLFSSH_CERTS +static int test_PrepareUserAuthRequestMlDsaCert_Params(word32 keygenLevel, + byte keyId, int derBufSz) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WS_UserAuthData authData; + WS_KeySignature keySig; + byte* derKey = NULL; + word32 payloadSz; + int derKeySz; + int result = 0; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) return -920; + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -921; goto done; } + + derKey = (byte*)WMALLOC(derBufSz, NULL, 0); + if (derKey == NULL) { result = -922; goto done; } + + derKeySz = wolfSSH_MakeMlDsaKey(derKey, (word32)derBufSz, keygenLevel); + if (derKeySz < 0) { result = -923; goto done; } + + /* Success path: good key, hasSignature=0 */ + WMEMSET(&authData, 0, sizeof(authData)); + WMEMSET(&keySig, 0, sizeof(keySig)); + payloadSz = 0; + authData.sf.publicKey.privateKey = derKey; + authData.sf.publicKey.privateKeySz = (word32)derKeySz; + authData.sf.publicKey.hasSignature = 0; + keySig.keyId = keyId; + keySig.heap = NULL; + ret = wolfSSH_TestPrepareUserAuthRequestMlDsaCert(ssh, &payloadSz, + &authData, &keySig); + if (ret != WS_SUCCESS) { result = -924; goto done; } + wc_MlDsaKey_Free(&keySig.ks.mldsa.key); + + /* Bad key: exercises the PrivateKeyDecode failure free path */ + { + static const byte badKey[] = { 0xFF, 0xFE, 0x00, 0x01 }; + WMEMSET(&authData, 0, sizeof(authData)); + WMEMSET(&keySig, 0, sizeof(keySig)); + payloadSz = 0; + authData.sf.publicKey.privateKey = badKey; + authData.sf.publicKey.privateKeySz = sizeof(badKey); + authData.sf.publicKey.hasSignature = 0; + keySig.keyId = keyId; + keySig.heap = NULL; + ret = wolfSSH_TestPrepareUserAuthRequestMlDsaCert(ssh, &payloadSz, + &authData, &keySig); + if (ret == WS_SUCCESS) { result = -925; goto done; } + } + + /* hasSignature=1 path: exercises sigSz accumulation */ + { + WMEMSET(&authData, 0, sizeof(authData)); + WMEMSET(&keySig, 0, sizeof(keySig)); + payloadSz = 0; + authData.sf.publicKey.privateKey = derKey; + authData.sf.publicKey.privateKeySz = (word32)derKeySz; + authData.sf.publicKey.hasSignature = 1; + keySig.keyId = keyId; + keySig.heap = NULL; + ret = wolfSSH_TestPrepareUserAuthRequestMlDsaCert(ssh, &payloadSz, + &authData, &keySig); + if (ret != WS_SUCCESS) { + wc_MlDsaKey_Free(&keySig.ks.mldsa.key); + result = -926; goto done; + } + if (keySig.sigSz == 0) { + wc_MlDsaKey_Free(&keySig.ks.mldsa.key); + result = -927; goto done; + } + if (payloadSz == 0) { + wc_MlDsaKey_Free(&keySig.ks.mldsa.key); + result = -928; goto done; + } + wc_MlDsaKey_Free(&keySig.ks.mldsa.key); + } + +done: + WFREE(derKey, NULL, 0); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); return result; @@ -8534,6 +9929,10 @@ static int test_IdentifyAsn1Key(void) ret = IdentifyAsn1Key(unitTestMlDsaPrivKey, (word32)sizeof(unitTestMlDsaPrivKey), 1, NULL, NULL); + /* unitTestMlDsaPrivKey encodes a level-44 key; the private-key decode + * path determines keyId purely from the decoded key's level (see + * IdentifyAsn1Key), not from WOLFSSH_NO_MLDSA44, so this must always + * be ID_MLDSA44 regardless of which individual levels are compiled in. */ if (ret != ID_MLDSA44) { printf("IdentifyAsn1Key: MlDsa priv failed, ret=%d\n", ret); result = -606; @@ -11990,7 +13389,24 @@ int wolfSSH_UnitTest(int argc, char** argv) printf("DoUserAuthRequestMlDsa: %s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); testResult = testResult || (unitResult != 0); + unitResult = test_DoUserAuthRequestMlDsaComposite(); + printf("DoUserAuthRequestMlDsaComposite: %s (result=%d)\n", + (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); + testResult = testResult || (unitResult != 0); + unitResult = test_SignHMlDsaComposite(); + printf("SignHMlDsaComposite: %s (result=%d)\n", + (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); + testResult = testResult || (unitResult != 0); + unitResult = test_PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope(); + printf("PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope: " + "%s (result=%d)\n", + (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); + testResult = testResult || (unitResult != 0); #ifdef WOLFSSH_KEYGEN + unitResult = test_BuildUserAuthRequestMlDsaComposite(); + printf("BuildUserAuthRequestMlDsaComposite: %s (result=%d)\n", + (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); + testResult = testResult || (unitResult != 0); unitResult = test_PrepareUserAuthRequestMlDsa(); printf("PrepareUserAuthRequestMlDsa: %s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); @@ -12205,7 +13621,21 @@ int wolfSSH_UnitTest(int argc, char** argv) unitResult = test_MlDsaKeyGen(); printf("MlDsaKeyGen: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + unitResult = test_MlDsaCompositeKeyGen(); + printf("MlDsaCompositeKeyGen: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; #endif +#endif /* WOLFSSH_KEYGEN */ + unitResult = test_OpenSshPemNegative(); + printf("OpenSshPemNegative: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + unitResult = test_OpenSshFormatNonCompositeRejected(); + printf("OpenSshFormatNonCompositeRejected: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; #endif wolfSSH_Cleanup(); diff --git a/wolfssh/error.h b/wolfssh/error.h index ff3a4b607..531c4a816 100644 --- a/wolfssh/error.h +++ b/wolfssh/error.h @@ -138,8 +138,9 @@ enum WS_ErrorCodes { WS_KDF_E = -1097, /* KDF error*/ WS_DISCONNECT = -1098, /* peer sent disconnect */ WS_MLDSA_E = -1099, /* MLDSA failure */ + WS_ED448_E = -1100, /* Ed448 failure */ - WS_LAST_E = WS_MLDSA_E /* Last error indicator */ + WS_LAST_E = WS_ED448_E /* Update to indicate last error */ }; diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 267319053..dee80fcc7 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -39,6 +39,9 @@ #include #include #include +#ifdef HAVE_ED448 +#include +#endif #ifndef WOLFSSL_WOLFSSH #error "wolfssh requires wolfSSL built with WOLFSSL_WOLFSSH" @@ -458,6 +461,13 @@ enum { ID_MLDSA44, ID_MLDSA65, ID_MLDSA87, + /* always declared; NameIdMap/WS_GetCompositeParams() gate reachability */ + ID_MLDSA44_ES256, + ID_MLDSA65_ES256, + ID_MLDSA87_ES384, + ID_MLDSA44_ED25519, + ID_MLDSA65_ED25519, + ID_MLDSA87_ED448, #endif ID_X509V3_SSH_RSA, ID_X509V3_ECDSA_SHA2_NISTP256, @@ -649,7 +659,9 @@ enum NameIdType { #define WOLFSSH_MAX_FILE_SIZE (1024ul * 1024ul * 4) #endif #ifndef WOLFSSH_MAX_PVT_KEYS - #define WOLFSSH_MAX_PVT_KEYS 8 + /* 3 default host keys (RSA, ECDSA, Ed25519) + 3 plain ML-DSA levels + + * 6 ML-DSA composite combos, with headroom for future combos. */ + #define WOLFSSH_MAX_PVT_KEYS 16 #endif #ifndef WOLFSSH_MAX_PUB_KEY_ALGO #define WOLFSSH_MAX_PUB_KEY_ALGO (WOLFSSH_MAX_PVT_KEYS + 2) @@ -1247,6 +1259,31 @@ WOLFSSH_LOCAL int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId); WOLFSSH_LOCAL int wolfSSH_FwdWorker(WOLFSSH* ssh); +#ifndef WOLFSSH_NO_MLDSA +/* Shared shape for a composite key's ML-DSA+traditional pair; reused + * in src/internal.c so the copies can't drift apart. */ +typedef struct WS_MlDsaCompositeBody { + MlDsaKey mldsa; + union { +#ifndef WOLFSSH_NO_ECDSA + ecc_key ecc; +#endif +#ifndef WOLFSSH_NO_ED25519 + ed25519_key ed25519; +#endif +#ifdef HAVE_ED448 + ed448_key ed448; +#endif +#if defined(WOLFSSH_NO_ECDSA) && defined(WOLFSSH_NO_ED25519) && \ + !defined(HAVE_ED448) + /* keep union non-empty (empty union rejected by some + * compilers) though unusable without a trad component */ + byte placeholder; +#endif + } trad; +} WS_MlDsaCompositeBody; +#endif /* WOLFSSH_NO_MLDSA */ + typedef struct WS_KeySignature { byte keyId; byte sigId; @@ -1276,10 +1313,92 @@ typedef struct WS_KeySignature { struct { MlDsaKey key; } mldsa; + WS_MlDsaCompositeBody mldsa_composite; #endif /* WOLFSSH_NO_MLDSA */ } ks; } WS_KeySignature; +#ifndef WOLFSSH_NO_ECDSA +#define ECDSA_ASN_SIG_SZ 256 +#endif + +#ifndef WOLFSSH_NO_MLDSA +#define TRAD_TYPE_ECC 1 +#define TRAD_TYPE_ED25519 2 +#define TRAD_TYPE_ED448 3 +#define COMPOSITE_DOMAIN_PREFIX "CompositeAlgorithmSignatures2025" +#define COMPOSITE_DOMAIN_PREFIX_SZ 32 +/* worst-case label size across all currently defined composite combos */ +#define COMPOSITE_MAX_LABEL_SZ 33 +#define ECC_P256_COORD_SZ 32 +#define ECC_P384_COORD_SZ 48 +/* worst-case trad public key size: P-384 uncompressed point */ +#define COMPOSITE_MAX_TRAD_PUB_SZ (1 + (2 * ECC_P384_COORD_SZ)) +/* worst-case trad private key size: Ed448 seed, else P-384 scalar */ +#ifdef HAVE_ED448 +#define COMPOSITE_MAX_TRAD_PRIV_SZ ED448_KEY_SIZE +#else +#define COMPOSITE_MAX_TRAD_PRIV_SZ ECC_P384_COORD_SZ +#endif +/* worst-case trad signature size: Ed448, else P-384 raw r/s wire format + * (length-prefixed r and s, each with up to 1 byte of sign-byte padding) */ +#ifdef HAVE_ED448 +#define COMPOSITE_MAX_TRAD_SIG_SZ ED448_SIG_SIZE +#else +#define COMPOSITE_MAX_TRAD_SIG_SZ (2 * (LENGTH_SZ + ECC_P384_COORD_SZ + 1)) +#endif +/* defensive slack on top of BuildUserAuthRequestMlDsaComposite()'s + * worst-case signature size; not load-bearing */ +#define COMPOSITE_SIG_ALLOC_SLACK_SZ 32 + +typedef struct CompositeParams { + const char* label; + word32 mldsaSigSz; + word32 mldsaPubSz; + word32 tradHashSz; + word32 labelSz; + word32 tradPubSz; + word32 tradSigSz; + word32 tradPrivSz; + enum wc_HashType tradHashId; + byte keyId; + byte mldsaLevel; + byte tradType; + int eccCurveId; +} CompositeParams; + +/* dispatch table for a composite key's trad (ECC/Ed25519/Ed448) half; see + * WS_GetTradOps() in src/internal.c */ +typedef struct CompositeTradOps { + int (*init)(void* key, void* heap); + void (*free)(void* key); + int (*importPub)(void* key, const byte* pub, word32 pubSz); + int (*importPriv)(void* key, const byte* priv, word32 privSz, + const byte* pub, word32 pubSz); + int (*exportPrivOnly)(void* key, byte* out, word32* outSz); + int (*exportPub)(void* key, byte* out, word32* outSz); + int (*sign)(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* mPrime, word32 mPrimeLen, + byte* wireSig, word32* wireSigSz); + int (*verify)(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* mPrime, word32 mPrimeLen); + byte tradType; +} CompositeTradOps; + +WOLFSSH_LOCAL int WS_GetCompositeParams(byte keyId, CompositeParams* params); +WOLFSSH_LOCAL const CompositeTradOps* WS_GetTradOps(byte tradType); +/* Inits both halves of a composite key pair; mldsaInit/tradInit track + * which succeeded so the caller can clean up correctly on failure. */ +WOLFSSH_LOCAL int InitCompositeKeyPair(const CompositeParams* params, + MlDsaKey* mldsa, void* tradKey, const CompositeTradOps* ops, + void* heap, int* mldsaInit, int* tradInit); +WOLFSSH_LOCAL int WS_Hash_Helper(enum wc_HashType hashId, const byte* msg, + word32 msgSz, byte* hash, word32 hashSz); +#endif + WOLFSSH_LOCAL int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, WS_KeySignature **pkey); WOLFSSH_LOCAL void wolfSSH_KEY_clean(WS_KeySignature* key); @@ -1290,6 +1409,8 @@ WOLFSSH_LOCAL int GetOpenSshKey(WS_KeySignature *key, WOLFSSH_LOCAL int GetOpenSshPublicKey(WS_KeySignature *key, const byte* buf, word32 len, word32* idx); #endif +WOLFSSH_LOCAL int WS_StripOpenSshPem(const byte* in, word32 inSz, + byte* out, word32* outSz); /* Parsing functions */ @@ -1723,6 +1844,17 @@ enum WS_MessageIdLimits { WOLFSSH_API int wolfSSH_TestBuildUserAuthRequestMlDsa(WOLFSSH* ssh, byte* output, word32* idx, const WS_UserAuthData* authData, const byte* sigStart, word32 sigStartIdx, WS_KeySignature* keySig); + WOLFSSH_API int wolfSSH_TestDoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + WS_UserAuthData* authData, byte keyId, word32 pubKeyBlobSz); + WOLFSSH_API int wolfSSH_TestPrepareUserAuthRequestMlDsaComposite( + WOLFSSH* ssh, word32* payloadSz, const WS_UserAuthData* authData, + WS_KeySignature* keySig); + WOLFSSH_API int wolfSSH_TestSignHMlDsaComposite(WOLFSSH* ssh, byte* sig, + word32* sigSz, byte keyId); + WOLFSSH_API int wolfSSH_TestBuildUserAuthRequestMlDsaComposite( + WOLFSSH* ssh, byte* output, word32* idx, + const WS_UserAuthData* authData, const byte* sigStart, + word32 sigStartIdx, WS_KeySignature* keySig); #endif /* !WOLFSSH_NO_MLDSA */ #if defined(WOLFSSH_SCP) && !defined(WOLFSSH_SCP_USER_CALLBACKS) WOLFSSH_API int wolfSSH_TestScpExtractFileName(const char* filePath, diff --git a/wolfssh/keygen.h b/wolfssh/keygen.h index 9116194d1..7cba8ee9f 100644 --- a/wolfssh/keygen.h +++ b/wolfssh/keygen.h @@ -46,12 +46,22 @@ extern "C" { #define WOLFSSH_MLDSAKEY_65 65 #define WOLFSSH_MLDSAKEY_87 87 +/* Traditional algorithm paired with the ML-DSA level; not every pair is + * valid -- see WS_GetCompositeParams() in internal.c for the list. */ +#define WOLFSSH_COMPOSITE_TRAD_ECDSA 1 +#define WOLFSSH_COMPOSITE_TRAD_ED25519 2 +#define WOLFSSH_COMPOSITE_TRAD_ED448 3 + WOLFSSH_API int wolfSSH_MakeRsaKey(byte* out, word32 outSz, word32 size, word32 e); WOLFSSH_API int wolfSSH_MakeEcdsaKey(byte* out, word32 outSz, word32 size); WOLFSSH_API int wolfSSH_MakeEd25519Key(byte* out, word32 outSz, word32 size); WOLFSSH_API int wolfSSH_MakeMlDsaKey(byte* out, word32 outSz, word32 level); +/* Writes an OpenSSH-format PEM key for the given level/trad pairing; + * out == NULL queries required size. Returns length or negative WS_*. */ +WOLFSSH_API int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, + word32 level, word32 tradType); #ifdef __cplusplus