diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 4ed6bd81a..a84602ba8 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", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384", + #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", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448", + #endif }; const int NUM_ALLOWED_TYPES = (int)(sizeof(allowedTypes) / sizeof(allowedTypes[0])); @@ -996,6 +1055,78 @@ 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) { + WFREE(der, heap, DYNTYPE_SSHD); + + *privBuf = data; + *privBufSz = dataSz; + + /* wstrnstr() stops at the first NUL, so binary buffers fall + * through to the WMEMCMP magic check below. */ + if (WSTRNSTR((const char*)*privBuf, + "-----BEGIN OPENSSH PRIVATE KEY-----", *privBufSz) != NULL) { + 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 { + *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..8f28e7d8c 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -27,6 +27,8 @@ #define WOLFSSH_USER_GET_STRING(x) #x #define WOLFSSH_USER_STRING(x) WOLFSSH_USER_GET_STRING(x) +#include /* for wc_KeyPemToDer */ + #if 0 typedef struct USER_NODE USER_NODE; @@ -101,6 +103,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); @@ -117,6 +125,7 @@ int SearchForPubKey(const char* path, const char* authKeysFile, const char* user, const WS_UserAuthData_PublicKey* pubKeyCtx, WUID_T uid, int strictModes); +word32 wolfsshd_test_MaxLineSz(void); #endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) int CheckPasswordHashUnix(const char* input, char* stored); diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 4f9d6c44a..5ff719e90 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,268 @@ 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", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384", + #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", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448", + #endif + }; + static const char keyAStr[] = "wolfssh-auth-key-test-A-AAAAAAA"; + static const char keyBStr[] = "wolfssh-auth-key-test-B-BBBBBBB"; + const byte* keyA = (const byte*)keyAStr; + const byte* keyB = (const byte*)keyBStr; + const word32 keySz = (word32)(sizeof(keyAStr) - 1); + 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); + /* Non-matching key: a recognized type must proceed to the key + * comparison and report a plain auth failure, not a negative error. */ + rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), + keyB, keySz); + if (rc == WSSHD_AUTH_FAILURE) { + 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"; + #elif defined(HAVE_ED448) + type = "ssh-mldsa87-ed448"; + #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"; + #elif !defined(WOLFSSH_NO_ED25519) && !defined(NO_SHA512) + type = "ssh-mldsa65-ed25519"; + #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"; + #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 +3985,225 @@ 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 (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; + } + } + + return ret; +} + const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_ParseConfigLine), @@ -3747,8 +4229,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..419e9f4f3 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,39 @@ 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) { - ret = WS_MEMORY_E; + int keyFormat = wolfSSHD_DetectPrivKeyFormat(data, dataSz, + heap, &keyDer, &privBuf, &privBufSz); + + 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..f05e59abb 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; @@ -3266,6 +3350,69 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) { const char* bufName = NULL; int loadDefaultHostKeys = 1; + #ifndef WOLFSSH_NO_MLDSA + /* Defaults + requested composite keys can exceed + * WOLFSSH_MAX_PVT_KEYS; skip defaults if loading both would overflow. */ + if (keyList != NULL && WSTRSTR(keyList, "mldsa") != NULL) { + if (WSTRSTR(keyList, "rsa") == NULL && + WSTRSTR(keyList, "ecdsa") == NULL && + /* "ssh-ed25519" not "ed25519": composite combo names + * would false-positive on the bare substring. */ + WSTRSTR(keyList, "ssh-ed25519") == NULL) { + loadDefaultHostKeys = 0; + } + else { + int defaultKeyCount = 1; /* load_key(peerEcc, ...) */ + int compositeMatchCount = 0; + int plainMlDsaMatchCount = 0; + word32 idx; + + #if !defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_ECC) + defaultKeyCount++; /* load_key(!peerEcc, ...) */ + #endif + #ifndef WOLFSSH_NO_ED25519 + defaultKeyCount++; /* load_key_ed25519() */ + #endif + for (idx = 0; + mldsaCompositeEntries[idx].substr != NULL; idx++) { + if (WSTRSTR(keyList, + mldsaCompositeEntries[idx].substr) != NULL) { + compositeMatchCount++; + } + } + + /* Plain ssh-mldsa-NN keys are loaded below via + * LoadMlDsaHostKeys() and consume slots too. */ + #ifndef WOLFSSH_NO_MLDSA44 + if (WSTRSTR(keyList, "mldsa-44") != NULL) { + plainMlDsaMatchCount++; + } + #endif + #ifndef WOLFSSH_NO_MLDSA65 + if (WSTRSTR(keyList, "mldsa-65") != NULL) { + plainMlDsaMatchCount++; + } + #endif + #ifndef WOLFSSH_NO_MLDSA87 + if (WSTRSTR(keyList, "mldsa-87") != NULL) { + plainMlDsaMatchCount++; + } + #endif + + if (defaultKeyCount + compositeMatchCount + + plainMlDsaMatchCount > WOLFSSH_MAX_PVT_KEYS) { + fprintf(stderr, + "Default host keys (%d) + requested ML-DSA " + "keys (%d) would exceed WOLFSSH_MAX_PVT_KEYS " + "(%d); skipping default host keys.\n", + defaultKeyCount, + compositeMatchCount + plainMlDsaMatchCount, + WOLFSSH_MAX_PVT_KEYS); + loadDefaultHostKeys = 0; + } + } + } + #endif #ifndef WOLFSSH_SMALL_STACK byte buf[EXAMPLE_KEYLOAD_BUFFER_SZ]; #endif @@ -3363,13 +3510,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..4ddbb8ff3 --- /dev/null +++ b/keys/server-key-mldsa44.pem @@ -0,0 +1,83 @@ +-----BEGIN PRIVATE KEY----- +MIIPPAIBATALBglghkgBZQMEAxEEggoEBIIKAKLCVqyUz3KV63e1qZvszchzAeKI +ngJybyMeO9ru+Tt8WGwYTqafMqiDSTU0apxsZ/SuMir/RRY6VlIUgz2yuVCYX6vr +jq6YcOPaOimaAqSdmtRh/SUUTcP7DBp5bx1U9VYfKCjuqHGkSIDn3IG/+gVj/PuR +tLrtxPZ4SeOjITy3C7YNYYJoCEUl0wBOBJFMAMdpA5YQIbWJ5JIs4MYAVCSMIBiK +EcUEUSAOBImNC6hAyxaRoShJmCBgoqgoZCRExKJBYhhQCKWREqOIGANE2EKOQ6Zw +wSYJGxRsY0RggzJqYEBADBBqgxJyYzhQEYNQyohFShgiAcaQwwgN2iAQE0MxkUZQ +ohRmSjCEUUJgmbJs0zhIITdui5glFLmFEjgiYzaFEBMsEMdIgbAMSMaMkhYIIDQo +VCBBAMAoCYmAATgqCTQBA4KQ0aRICEBKAkaFnARFmgAiwUBR2JIoiMBsAglQ0IIo +ERZIwkQJyDiAFLYsSUSAiDRAE5kBEUVFAshFzCQFykaFybhMBBkkI4JMAZRJHLZw +0xYQZAJxG6cEA6mIGakAjKiFAxVoGSVoIMNJCQCQSRBMgJYFA6NBk7YRkKIplARO +C5OF2MZoYUgAGIFxopKMyZJgIgdMJJZEUKBNETRGIShwYkZuJKURE5IQ45RMBDEK +miYhY6QBgEhJkCBuYbIwQ6QRmzIOZBiBBBmQFLlFBDYSAwFmAaEogDIpTKgASkaN +GUFSCilhm8gkE5Mo0cIoAaKFo5IE4EAI3KJoCqksmQQMoAIy46ZhAhmKEqUNHMgB +WsJpi5KQZDgxi0hm2JBp3EKIG8ZE5EZoCSNiGzQSiAaJyBRgSDgR2gBygiACoygs +2gQo2EZNyDBI4DCICkAQBJVFRLYoAYgMSoRAxIQs0gRO4yCAmAaKA7VFQKQAkwQg +HAlJAMBAFDIwQsZF1EQsIyEh4UYpXMIgiRYkWcSBYcQkSyIkBJVNyrZQ4iIIzERM +w4gtDLREIxQwiUYxA6NpoLhEQcZs07AQBBYMGRclyCYtGjUmwJSNALiQUAiBwCBJ +C5iJEDQtAUdsEhhBCIOQobKRmQBmyxYtwyItApCBQMKMA8OBogZEHBZt2Lhh4xgB +G0VtDBAh1KQESaSIRCZFQoIJAzEw4MRggjSFECcp2JRNgqKF4DhuyYiQEaiMIScB +VEJggRBN1CAAJINJ2Kbr1PtLlqlPA1sNL7vjN5TSlPtweivAADWsdXmBGQdmc1ht +9/YbeP95e/tGkNs2UbyjSR6I8EiObYD9c6PdwoI6CaduboIx1NsDp4dGZS7Zz/8Y +VW0U/DcpK3WTq1jPQzjH5MTyHN8fSa2IWPz86jlUxqb9wYslkb7BBi27zCV325gp +7MtmgHzpt7C0i1Oi9/f4hP3ONA5DBy4uJ/l2gphZNTVWl6py62TXE9AwMHAHhXAV +ViyR4JTWi/yF2+RnpCi2xk1pJHCb6JNuUQTzTQRfUprJ9FGzrvm8N6l9LOGbTaKq +QJeFgEpWK/GmIeju0QZGaN/XgIYoYMglKkpGIfPLPFcY3Q6Sx3AAHHQNfRtJsY7g +HrJOV8cwsIt4AzXzKNgEhoIGOkJ8acXXV3bzS4YIRilSzyxQm+zrIq+JOSUp12xr +5LFgtVUJOiiQSCaxu2Fcp3GxHwWKNjX4CQBIc0weNnHExN7m7M0cxar/j8Weayr5 +aCbUTECpKd6tQz4HtNK30IsS2E9iRsGrSQHP6YquVc+FOutRx/cp2WyACFRIB2/c +GjkLgnlhPHiGOeSXqXZ7ZvrQu1lysy3jPKBCq0dJNTL/Q4rGx2ZeKdNt0IafeFqO +fPifo9CCqxNt4h1dfnyrNCajbEQu4gim9Lae8uzEwiTqNggQHEX1FZdUzjnaMmoH +20Vtt1TMTP2xc8l5Hrwxob30be3LzmZPknhsk2AOsytQ5rgrC7HhJDqySZq56ya3 +rzWFTI+c5YaF4Zn9mD3TAuNqGlNA492jsAfltp4rlqvjqupbyKejqRvJeUQ30Pm2 +7Xn4cOOuZ8lA/GkU1f9mA/n2OgMau4Hzd5tnEDt6KkngNmtsZJc38KZJYYE0KXY2 +U1Z+Fonvti1WGEbo7ffazju7E7biz4W/F9FmUoFXLJ0e48g0PUzQ5jYsf72T3bOn +mbUxfPzecCttj1etNgPPF0eTKmE60YhQ0qH31KHW2SmHcDIosfMlniPbO5E6EbtL +9Kpv/KJyfatpxcww0OwFyH62kGcTySH/S7kMha7FxUvonsUnPP/MHS5MIHhfRAxj +tf90ycfffm2Jh7uOjto2WI799dzmz+8t9HHwOPiacYy/MDa7ngWlNC8L/qdtSTOk +Ims4J6u+oaf52tq0hDhnqRZYgcCUAEi+qr/+Tpt8uQzvVasbmCToOMO278UJol7P +0PjY93n7bAGFDLoavx7GW67XLA7J22qmudyg44QNNLsaGcQFzaweaCvzqjK5MliA +FlPND9uOAkBA4n6fBwyglTcmpA5jVkEn5xwp4S52D098jOJRhpx4h3tW/RKdeZw7 +EotdhD4PTOErWP95E21LODdnUptaRp1l8puaD2V4oIkBqzg0VdgJYVqVwp3KLt0t +6UhjhReFrQ7mSXEXT5C+pduR4zOTovr294EAPU2gHpFyUQs1+ZINFqBOcJZiU1H/ +Ebm8jtVc3nBMgVQTCh/WDrb2XCSNSMFVkkopLwuzBlmJKXhT/QwLlac4ED7bVWJV +mb8eu3PqRB2BY9H/6cbXxEWYmwqxk3VJnfEdpNixytt7wLHnjXnrec/OK/l+xTEL +hu2wASWA6B59QvoaqggQrfCH0+xpe/Dyh10w3oa6F18NvBd3Kw231te3ujTLYFgh +e1WXnPTHooprQSq6N1Xv6TQap6LVPF1o5mKT5iwZV5KCYIeM7WJ423xkWxaf3+KQ +jT2p67Z/nAHyTjEFuAn7cTxjwA8FaAd7DD6PYQVgLscMfQ11e8d904lnXqkfCQtL +9MVbyOx+i1vGSnjILMPfi/n7kOukW5Ts3z0Z4rbs/7fTu3BxJxpJFegjA5DmEVqU +jy3uaydiNTCe0ExcG98DUFq6fd7lciiAOqCfibMRRhgdWGMHMGlw7oAKihl9jlkH +i3JI+cchOS7toMttB0baHndVLxJC2Hm+mGSA4SjeGiwFUbMACm6jDoNZk/LFHrVS +ZsStjWV6SOE8EAPp+L4Fj1vuY5G3EIZ78dqbWDvqVUvrspOfQ+RutzVpbZRchrfW +lF/ONhsPteNXb6WWM23bRrmPi31yKiERxBFs+/EzD4rnifznvA6kqnKZAzi8SPoo +EAsmZbm27IZtDMUZmW2nUwbryi9swriS57ZCeXuaNbvDKTnSnRF4OePFHns6tWXB +pZf2v5QH+9uRTCHp7kRcWi8RuoSsxfhITf8d9dxyBD2eWmiuvgcE5AIn4LyBggUg +osJWrJTPcpXrd7Wpm+zNyHMB4oieAnJvIx472u75O3yVBtYE6g3kcSFD7EQ/iUoU +CtPwp3eJnvHgr1D83MA7pOF3OEQW6fLbKRbbrJtEUg4owXgOTCvuN1d21Fv2xVQc +Cvf8V8tPVGoTco++y8fBAZnWif6IR5iTCE9tIKibiETLuF7kG+MtqvT5soprR9OB ++5wCxKKIuBpPMK8Q1s4GMqPsJhGP/ey5FQlXJHeRvRxGaoosdX8ae8D11g0lgLXC +F7DuBE5yS9zsr1INmqAvbpS24xG3Q5aDgMF+a6zbOHE3tD+PXPAD4SlbJ9EV3HCt +ln1DnEwHNow9fVQwev+hc8ZU5Rv/q86OwVF0mZ20Bng+TbC1ucW5HArWLQfA2M3A +WUk0X0h+a/+YDkcjRcFQc8Vb1uJgDD73DNn+L9qD1bPOwLkV/0ZhUYdDcqcB/vKe +Sp5twP9Z8ixi6RX2mrmL14ERc+CBVsvTDUnEN1PcOSrlAM9AYwlMq4DKXISyz8Rb +D2MCHzirdJt6K9apELznmUULL/0Fa9UcP26EbrIW4qJ6KwGQwru5zoDHNZcnmIF2 +K+Vzi24KmkPaR1hbOjcFiYN/6Ek+9R6f0ptCGQE2QaDcjWDzcCtvXzGwpGtSE6mT +zcY3Sf+SWeohONTf4MdRTFZOUe9USs/H5+4pU0humG/yEi0cj3Wnb+bnLkj6lHlF +kEujxla1UXbhPYAxeMf74VdDpKn8JdkjuSpy/wZHphs2DGXPRTezypIr8dq5eMcX +fRvFHywa7SB2KZwg+ewjMdiGX0wo54OtCD55P+kzu4DwggMDJk39Kxam6HvkWMpA +pGBPYsvq5azCCo8jSJOZNOklpv3s09Hr2Emim34qEeu56UHLSPMHhjmIPtCm96TV +xoSFi6BEk46cJpNFFx9TFbqCD8zmULbaHMKBOPjLbPVA44qBQDtHieqsMvTxpKRG +jLDrJQVWB+ZbD/nhbBXfdmJek3CNeki9pfKiGgpzkUUAXKWo4XDQIEzE4e8FTE52 +wlN3qtY7AWp6IyYwBuRAIxxk+0dTpKcaFgYLmNqmJU+TfgOdaamTSk110kAHyw+D +X9bdAHSJplHodQOaCdTYD4WhzeyfveJk16b/uoyL02lcVNaQbN3J0E6nPk9qCI0M +pn1voU1JiGwTlOz+PtiqZdk9Q/awfF6VqZJ3Q4fIxMbdFi7dcEdALH9mW7mIgSpm +CajsV3Cy+va9nZalRBTr9ZIO3WTB8wNMMXN7D48+JLDLKc0IfNk4sL/DRz4+fl9V +vqwkCMgFV0wfR3zLsQswmZPXEWhxkp4sFaBWDgnaCO9DsU5eepaVnhcCIdGyxJR5 +JzIsbFRQ8m7BFMe/IjqfMrztgX/tbvCDQM5E0QPoUXeASxVeBFearTwdoszlMBIn +gNFbmbVGAhRAQ/GnzRjc2j6sCrJlvPKjFbG8qaQwOCLRLlKZqCEe0hHOSJVZ0sIL +BG83WHjEy/9Y3Lwa2uDZlRQPfOBrZVq+mScYCGyhXqcREsIdseplxnAD80NPWHGo ++HN5VB+cG/4NwW1vhuZYrQBrA50/3uCYn9QeclfTjm6GVl3NRG+KOLykjvyBcBHC +3+2ql9h2GLrilNAVZ7SNTXO+KIqQCgL8tCiq5n7RJRSwtalwfPTmuwgKSXwMjMi6 +ixaiLxhn/0OhJhDrg1c9ISZS50FOvskD9DlSfZz3SNLoTF/+hxIRCGyxOrSKefox +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..c2af21c74 Binary files /dev/null and b/keys/server-key-mldsa44es256 differ diff --git a/keys/server-key-mldsa65ed25519 b/keys/server-key-mldsa65ed25519 new file mode 100644 index 000000000..f53e5615e Binary files /dev/null and b/keys/server-key-mldsa65ed25519 differ diff --git a/keys/server-key-mldsa65es256 b/keys/server-key-mldsa65es256 new file mode 100644 index 000000000..117ceff94 Binary files /dev/null and b/keys/server-key-mldsa65es256 differ diff --git a/keys/server-key-mldsa87ed448 b/keys/server-key-mldsa87ed448 new file mode 100644 index 000000000..07d7c7ee7 Binary files /dev/null and b/keys/server-key-mldsa87ed448 differ diff --git a/keys/server-key-mldsa87es384 b/keys/server-key-mldsa87es384 new file mode 100644 index 000000000..22f3faf39 Binary files /dev/null and b/keys/server-key-mldsa87es384 differ diff --git a/src/internal.c b/src/internal.c index ea2bfa799..784d7dc76 100644 --- a/src/internal.c +++ b/src/internal.c @@ -65,6 +65,20 @@ #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 +524,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 +542,6 @@ const char* GetErrorString(int err) #endif } - static int wsHighwater(byte dir, void* ctx) { int ret = WS_SUCCESS; @@ -1017,6 +1033,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," +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa65-ed25519," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256," +#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," +#endif #ifndef WOLFSSH_NO_MLDSA87 "ssh-mldsa-87," #endif @@ -1770,6 +1808,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 +2060,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 +2608,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 +2620,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 +2682,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 +2694,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 +3082,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" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + { ID_MLDSA65_ES256, TYPE_KEY, "ssh-mldsa65-es256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + { ID_MLDSA87_ES384, TYPE_KEY, "ssh-mldsa87-es384" }, +#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" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + { ID_MLDSA87_ED448, TYPE_KEY, "ssh-mldsa87-ed448" }, +#endif #ifndef WOLFSSH_NO_MLDSA65 { ID_MLDSA65, TYPE_KEY, "ssh-mldsa-65" }, #endif @@ -4162,6 +4349,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 +4889,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 +5568,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 +5592,15 @@ 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 */ + byte q[WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; + word32 qSz; + } mldsa_composite; #endif } sk; }; @@ -5570,6 +5793,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 +6205,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 +6283,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 +7160,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 +8654,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 +9732,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 +13068,14 @@ struct wolfSSH_sigKeyBlockFull { #endif word32 qSz; } mldsa; + struct { + WS_MlDsaCompositeBody base; + byte tradInit; + /* largest mldsaPubSz + tradPubSz across + * WS_GetCompositeParams() combos; keep in sync */ + byte q[WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; + word32 qSz; + } mldsa_composite; #endif } sk; }; @@ -12782,7 +13083,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 +13727,163 @@ 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. */ + { + WS_KeySignature keySig; + word32 idx = 0; + + XMEMSET(&keySig, 0, sizeof(keySig)); + keySig.keyId = sigKeyBlock_ptr->pubKeyId; + 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); + if (ret == WS_SUCCESS) + 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); +#ifndef WOLFSSH_NO_ECDSA + if (params.tradType == TRAD_TYPE_ECC) { + byte eccPriv[MAX_ECC_BYTES]; + byte eccPub[MAX_ECC_BYTES * 2 + 1]; + word32 eccPrivSz = params.tradPrivSz; + word32 eccPubSz = params.tradPubSz; + + ret = wc_ecc_export_private_only( + &keySig.ks.mldsa_composite.trad.ecc, + eccPriv, &eccPrivSz); + if (ret == 0) { + ret = wc_ecc_export_x963( + &keySig.ks.mldsa_composite.trad.ecc, + eccPub, &eccPubSz); + } + if (ret == 0) { + ret = wc_ecc_init_ex(&sigKeyBlock_ptr-> + sk.mldsa_composite.base.trad.ecc, + heap, INVALID_DEVID); + if (ret == 0) { + sigKeyBlock_ptr->sk.mldsa_composite.tradInit = 1; + } + } + if (ret == 0) { + ret = wc_ecc_import_private_key(eccPriv, + eccPrivSz, eccPub, eccPubSz, + &sigKeyBlock_ptr-> + sk.mldsa_composite.base.trad.ecc); + } + wc_ecc_free(&keySig.ks.mldsa_composite.trad.ecc); + wc_ForceZero(eccPriv, sizeof(eccPriv)); + /* caller's cleanup frees base.mldsa/.trad on + * failure too; don't free here. */ + if (ret != 0) { + ret = WS_CRYPTO_FAILED; + } + } + else +#endif + { + const CompositeTradOps* tradOps = + WS_GetTradOps(params.tradType); + WS_MlDsaCompositeBody* base = + &sigKeyBlock_ptr->sk.mldsa_composite.base; + XMEMCPY(&base->trad, + &keySig.ks.mldsa_composite.trad, + sizeof(base->trad)); + sigKeyBlock_ptr->sk.mldsa_composite.tradInit = 1; + if (tradOps != NULL) { + tradOps->free(&keySig.ks.mldsa_composite.trad); + } + } + } + } + + 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 +15034,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 +15374,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 +15530,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 +18219,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 +18436,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 +18620,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 +20926,1343 @@ 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; + 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; + 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; + 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_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, 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_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, + 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_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, + 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; + + 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(ssh->rng, (int)params.tradPrivSz, + &sigKey.sk.mldsa_composite.base.trad.ecc); +#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..9f6d4cf03 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,361 @@ 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 internal.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. */ + int curveId = (params->tradPrivSz <= 32) ? + ECC_SECP256R1 : ECC_SECP384R1; + + if (wc_ecc_init(&key) != 0) { + return WS_CRYPTO_FAILED; + } + ret = (wc_ecc_make_key_ex(rng, (int)params->tradPrivSz, &key, + curveId) == 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]; + byte mldsaPub[WC_MLDSA_87_PUB_KEY_SIZE]; + 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)); + } + if (outSz < b64Sz + WSTRLEN(header) + WSTRLEN(footer)) { + 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 */ + if (outSz > off) { + out[off] = '\0'; + } + ret = (int)off; + } + 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..8f7e89180 100644 --- a/src/ossh.c +++ b/src/ossh.c @@ -297,6 +297,67 @@ static int GetOpenSshKeyMlDsa(MlDsaKey* key, } return ret; } + +/* Parse OpenSSH ML-DSA composite private key blob; see + * GetOpenSshKeyPublicMlDsaComposite() for the public-key-only counterpart. + * 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_ImportPubRaw(mldsa, pub, params.mldsaPubSz); + } + if (ret == WS_SUCCESS) { + ret = wc_MlDsaKey_MakeKeyFromSeed(mldsa, priv); + } + if (ret == WS_SUCCESS) { + ret = ops->importPriv(tradKey, priv + MLDSA_SEED_SZ, params.tradPrivSz, + pub + params.mldsaPubSz, params.tradPubSz); + } + + if (ret != 0) { + wc_MlDsaKey_Free(mldsa); + ops->free(tradKey); + ret = WS_KEY_FORMAT_E; + } + return ret; +} #endif #ifdef WOLFSSH_TPM @@ -357,6 +418,54 @@ static int GetOpenSshKeyPublicMlDsa(MlDsaKey* key, const byte* buf, } return ret; } + +/* public-key-only counterpart to GetOpenSshKeyMlDsaComposite(); no trailing + * private-key string to parse. Returns WS_SUCCESS or negative WS_* error */ +static int GetOpenSshKeyPublicMlDsaComposite(byte keyId, MlDsaKey* mldsa, + void* tradKey, void* heap, const byte* buf, word32 len, word32* idx) +{ + int ret; + int mldsaInit = 0; + int tradInit = 0; + const byte* pub = NULL; + word32 pubSz = 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) { + ret = GetStringRef(&pubSz, &pub, buf, len, idx); + } + if (ret == 0) { + if (pubSz != (params.mldsaPubSz + params.tradPubSz)) { + ret = WS_KEY_FORMAT_E; + } + } + if (ret == 0) { + ret = wc_MlDsaKey_ImportPubRaw(mldsa, pub, params.mldsaPubSz); + } + if (ret == 0) { + ret = ops->importPub(tradKey, pub + params.mldsaPubSz, params.tradPubSz); + } + + if (ret != 0) { + if (mldsaInit) wc_MlDsaKey_Free(mldsa); + if (tradInit) { + ops->free(tradKey); + } + if (ret != WS_UNIMPLEMENTED_E && ret != WS_KEY_FORMAT_E) { + ret = WS_CRYPTO_FAILED; + } + } + return ret; +} #endif #ifndef WOLFSSH_NO_RSA static int GetOpenSshPublicKeyRsa(RsaKey* key, const byte* buf, word32 len, @@ -423,6 +532,17 @@ int GetOpenSshPublicKey(WS_KeySignature *key, ret = GetOpenSshKeyPublicMlDsa(&key->ks.mldsa.key, buf, len, idx, WC_ML_DSA_87); 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 = GetOpenSshKeyPublicMlDsaComposite(keyId, + &key->ks.mldsa_composite.mldsa, + &key->ks.mldsa_composite.trad, + key->heap, buf, len, idx); + break; #endif default: ret = WS_UNIMPLEMENTED_E; @@ -440,11 +560,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 +653,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 +713,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..ebded94aa 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,35 +2140,6 @@ 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; - } - - /* The begin marker must lead the buffer. */ - if (WMEMCMP(in, PrivBeginOpenSSH, beginSz) != 0) { - WLOG(WS_LOG_DEBUG, "OpenSSH private key missing begin marker"); - 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); @@ -2187,17 +2156,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 +2178,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..756b61541 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"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa65-es256"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa65-ed25519"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa87-es384"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa87-ed448"), + EXIT_SUCCESS); +#endif AssertIntEQ(wolfSSH_Cleanup(), WS_SUCCESS); diff --git a/tests/unit.c b/tests/unit.c index 8861f87b2..174f235eb 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 = -111; } } 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" + "AAAA\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,1134 @@ 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. + * Either tamper mode 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 + int keysz = (keyId == ID_MLDSA87_ES384) ? 48 : 32; + if (wc_ecc_init(&eccKey) != 0) { result = -713; goto done; } + eccInit = 1; + if (wc_ecc_make_key(&rng, keysz, + &eccKey) != 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 + } + + sigBlobSz = UINT32_SZ * 2 + keyTypeNameSz + mldsaSigSz + tradSigSz; + sigBlob = (byte*)WMALLOC(sigBlobSz, NULL, 0); + if (sigBlob == NULL) { result = -735; goto done; } + + off = 0; + MlDsaTest_PutLen(sigBlob + off, keyTypeNameSz); off += UINT32_SZ; + WMEMCPY(sigBlob + off, keyTypeName, keyTypeNameSz); off += keyTypeNameSz; + 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 == 1) { + /* Flip a byte in the trad-algorithm component of the signature + * (last byte of the blob) and confirm verification rejects it + * rather than accepting a corrupted composite signature. */ + sigBlob[sigBlobSz - 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 = sigBlobSz; + + 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", + 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", + 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", + 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", + ID_MLDSA65_ED25519); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa87-ed448", + 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; + + 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; + 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; + 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, 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); + +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; + 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; + } + +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", 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", 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", 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", 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", 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", + ID_MLDSA44_ES256, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256", + ID_MLDSA44_ES256, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256", + ID_MLDSA44_ES256, 2); + 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", + ID_MLDSA65_ES256, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256", + ID_MLDSA65_ES256, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256", + ID_MLDSA65_ES256, 2); + 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", + ID_MLDSA87_ES384, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384", + ID_MLDSA87_ES384, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384", + ID_MLDSA87_ES384, 2); + 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; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519", + ID_MLDSA65_ED25519, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519", + ID_MLDSA65_ED25519, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519", + ID_MLDSA65_ED25519, 2); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448", + ID_MLDSA87_ED448, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448", + ID_MLDSA87_ED448, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448", + ID_MLDSA87_ED448, 2); + 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 +9824,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 +13284,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 +13516,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..4d0429782 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, @@ -1247,6 +1257,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 +1311,90 @@ 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; +} 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 (*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 +1405,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 +1840,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