From ee3115d8d26f121b72d6aea1c66ab75f44fc9aab Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Thu, 16 Jul 2026 14:31:24 -0600 Subject: [PATCH 1/2] Add ML-DSA44-Ed25519 composite signature support --- apps/wolfsshd/auth.c | 104 +- apps/wolfsshd/auth.h | 8 + apps/wolfsshd/test/test_configuration.c | 257 +++- apps/wolfsshd/wolfsshd.c | 31 +- examples/echoserver/echoserver.c | 115 +- keys/include.am | 2 +- keys/server-key-mldsa44ed25519 | Bin 0 -> 2894 bytes src/internal.c | 1516 ++++++++++++++++++++++- src/keygen.c | 332 +++++ tests/kex.c | 7 + tests/unit.c | 1004 +++++++++++++++ wolfssh/internal.h | 112 ++ wolfssh/keygen.h | 11 + 13 files changed, 3465 insertions(+), 34 deletions(-) create mode 100644 keys/server-key-mldsa44ed25519 diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 793ee2dcc..6346b9afc 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -53,6 +53,7 @@ #include #include #include +#include #ifdef WOLFSSL_FPKI #include @@ -118,14 +119,43 @@ struct WOLFSSHD_AUTH { #endif #ifndef MAX_LINE_SZ - /* Sized to hold the largest authorized_keys entry. */ + /* Sized to hold the largest authorized_keys entry. Composite + * (ML-DSA + traditional) entries carry an extra COMPOSITE_MAX_TRAD_PUB_SZ + * bytes of traditional public key on top of the plain ML-DSA pubkey. */ #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) + /* An x509v3-ssh-mldsa-87 entry carries a full certificate: + * the ML-DSA-87 public key plus a CA's ML-DSA-87 signature + * plus DER overhead, all base64-encoded. */ + #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 @@ -231,9 +261,10 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, #endif #endif #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + "ssh-mldsa44-ed25519@openssh.com", + #endif }; - const int NUM_ALLOWED_TYPES = - (int)(sizeof(allowedTypes) / sizeof(allowedTypes[0])); int typeOk = 0; int i; @@ -250,8 +281,9 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, } } if (ret == WSSHD_AUTH_SUCCESS) { - for (i = 0; i < NUM_ALLOWED_TYPES; ++i) { - if (WSTRCMP(type, allowedTypes[i]) == 0) { + for (i = 0; i < (int)(sizeof(allowedTypes) / sizeof(allowedTypes[0])); + ++i) { + if (allowedTypes[i] != NULL && WSTRCMP(type, allowedTypes[i]) == 0) { typeOk = 1; break; } @@ -783,6 +815,62 @@ int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid, #endif } +/* Determine the wolfSSH_CTX_UsePrivateKey_buffer() format of a raw host + * private key buffer, and hand back the buffer/length to actually load. + * + * PEM-armored OpenSSH keys (BEGIN OPENSSH PRIVATE KEY) and raw binary + * openssh-key-v1 blobs are both loaded with WOLFSSH_FORMAT_OPENSSH; anything + * else (including a successfully PEM-decoded traditional key, which starts + * with a DER SEQUENCE tag) is loaded as WOLFSSH_FORMAT_ASN1. + * + * data - raw file contents (may be PEM or DER/OpenSSH binary). + * dataSz - size of data. + * der - scratch DerBuffer used when data is PEM; caller must + * wc_FreeDer() it after use. + * privBuf - set to the buffer to actually load (either data, or der's + * buffer if PEM decoding succeeded). + * privBufSz - set to the size of *privBuf. + * + * Returns WOLFSSH_FORMAT_ASN1 or WOLFSSH_FORMAT_OPENSSH. */ +int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, DerBuffer** der, + byte** privBuf, word32* privBufSz) +{ + int keyFormat = WOLFSSH_FORMAT_ASN1; + + if (data == NULL || dataSz == 0 || der == NULL || privBuf == NULL || + privBufSz == NULL) { + return WS_BAD_ARGUMENT; + } + + if (wc_PemToDer(data, dataSz, PRIVATEKEY_TYPE, der, NULL, NULL, NULL) + != 0) { + *privBuf = data; + *privBufSz = dataSz; + + if (WSTRNSTR((const char*)*privBuf, + "-----BEGIN OPENSSH PRIVATE KEY-----", *privBufSz) != NULL) { + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + else if (*privBufSz >= 15 && + WMEMCMP(*privBuf, "openssh-key-v1", 15) == 0) { + /* Compare 15 bytes: the magic includes a NUL. */ + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + } + else { + *privBuf = (*der)->buffer; + *privBufSz = (*der)->length; + /* wc_PemToDer decoded the PEM armor; if the result is an OpenSSH + * binary blob, select the matching format. */ + if (*privBufSz >= 15 && + WMEMCMP(*privBuf, "openssh-key-v1", 15) == 0) { + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + } + + return keyFormat; +} + static int SearchForPubKey(const char* path, const char* authKeysFile, const WS_UserAuthData_PublicKey* pubKeyCtx, WUID_T uid, int strictModes) diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index c487a68ca..ff01ab568 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -21,6 +21,9 @@ #ifndef WOLFAUTH_H #define WOLFAUTH_H +#include /* DerBuffer, used by + * wolfSSHD_DetectPrivKeyFormat() */ + #if 0 typedef struct USER_NODE USER_NODE; @@ -86,6 +89,11 @@ 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); +/* Shared by wolfsshd.c's SetupCTX() to classify a loaded host private key + * buffer as OpenSSH or ASN1/DER. See the definition in auth.c. */ +int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, DerBuffer** der, + byte** privBuf, word32* privBufSz); + #ifdef WOLFSSHD_UNIT_TEST #ifndef _WIN32 extern int (*wsshd_setregid_cb)(WGID_T, WGID_T); diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index aa5c410de..48cdfcae0 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -1312,18 +1312,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) { @@ -1337,6 +1338,108 @@ static int BuildAuthKeysLine(const byte* key, word32 keySz, return WS_SUCCESS; } +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 allowedTypes[]/NUM_ALLOWED_TYPES 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_ED25519) + "ssh-mldsa44-ed25519@openssh.com", + #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 rejected with a negative status. */ + 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 < 0) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + return WS_FATAL_ERROR; + } + + return WS_SUCCESS; +} + /* 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. */ @@ -1900,6 +2003,146 @@ static int test_OpenSecureFile(void) } #endif /* !_WIN32 */ +/* 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), and traditional + * PEM/DER keys. */ +static int test_DetectPrivKeyFormat(void) +{ + typedef struct { + const char* desc; + const char* file; + int wantFormat; + } DPK_CASE; + static const DPK_CASE cases[] = { + { "PEM-armored OpenSSH key", "id_ecdsa", WOLFSSH_FORMAT_OPENSSH }, + { "raw binary openssh-key-v1 composite ML-DSA key", + "server-key-mldsa44ed25519", WOLFSSH_FORMAT_OPENSSH }, + { "PEM traditional key decodes to DER/ASN1", "server-key-ecc.pem", + WOLFSSH_FORMAT_ASN1 }, + }; + word32 i; + int ret = WS_SUCCESS; + byte dummy = 0; + DerBuffer* badDer = 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. */ + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, 0, &badDer, &badPrivBuf, + &badPrivBufSz); + Log(" Testing scenario: 0-length buffer. %s\n", + (badGot == WS_BAD_ARGUMENT && badPrivBuf == NULL && badPrivBufSz == 0) + ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT || badPrivBuf != NULL || + badPrivBufSz != 0) { + return WS_FATAL_ERROR; + } + + badGot = wolfSSHD_DetectPrivKeyFormat(NULL, sizeof(dummy), &badDer, + &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; + } + + for (i = 0; i < (word32)(sizeof(cases) / sizeof(cases[0])); i++) { + char path[128]; + byte* data; + word32 dataSz = 0; + DerBuffer* der = 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, &der, &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; + } + + wc_FreeDer(&der); + free(data); + if (ret != WS_SUCCESS) { + return ret; + } + } + + return ret; +} + const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_ParseConfigLine), @@ -1921,8 +2164,10 @@ const TEST_CASE testCases[] = { #ifndef _WIN32 TEST_DECL(test_OpenSecureFile), #endif + TEST_DECL(test_DetectPrivKeyFormat), #ifdef WOLFSSL_BASE64_ENCODE TEST_DECL(test_CheckAuthKeysLine), + TEST_DECL(test_CheckAuthKeysLineTypes), #endif #ifndef _WIN32 TEST_DECL(test_GetUserGroupNames), diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 0776839d9..c13a51bf9 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -344,8 +344,8 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, { int ret = WS_SUCCESS; DerBuffer* der = NULL; - byte* privBuf; - word32 privBufSz; + byte* privBuf = NULL; + word32 privBufSz = 0; void* heap = NULL; if (ctx == NULL) { @@ -406,21 +406,26 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } if (ret == WS_SUCCESS) { - if (wc_PemToDer(data, dataSz, PRIVATEKEY_TYPE, &der, NULL, - NULL, NULL) != 0) { - wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Failed to convert host " - "private key from PEM. Assuming key in DER " - "format."); - privBuf = data; - privBufSz = dataSz; + int keyFormat = wolfSSHD_DetectPrivKeyFormat(data, dataSz, + &der, &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 { - privBuf = der->buffer; - privBufSz = der->length; + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " + "key as DER format."); } - if (wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, privBufSz, - WOLFSSH_FORMAT_ASN1) < 0) { + 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; diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index f86fbc75f..d28f398b9 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1845,8 +1845,74 @@ static int load_key_mldsa87(byte* buf, word32 bufSz) } #endif /* WOLFSSH_NO_MLDSA87 */ +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +/* When buf is NULL, *bufSz is set to the file's size (size-query mode); + * otherwise the file is read into buf (up to *bufSz bytes). */ +static int load_key_mldsa44_ed25519(byte* buf, word32* bufSz) +{ + word32 sz = 0; +#ifndef NO_FILESYSTEM + sz = load_file("./keys/server-key-mldsa44ed25519", buf, bufSz); +#else + (void)buf; (void)bufSz; +#endif + return sz; +} +#endif /* !WOLFSSH_NO_MLDSA44 && !WOLFSSH_NO_ED25519 */ #ifndef WOLFSSH_NO_MLDSA +/* Composite keys are OpenSSH-armored envelopes holding two keys, so their + * buffer must be sized from the actual file rather than a fixed constant + * like MLDSA_MAX_BOTH_KEY_DER_SIZE. */ +static int LoadMlDsaCompositeHostKey(WOLFSSH_CTX* ctx, + int (*loadFn)(byte*, word32*), const char* label) +{ + byte* compBuf = NULL; + word32 compBufSz = 0; + word32 compSz; + + loadFn(NULL, &compBufSz); + if (compBufSz == 0) { + fprintf(stderr, "Couldn't find size of %s key file.\n", label); + return -1; + } + compBuf = (byte*)WMALLOC(compBufSz, NULL, 0); + if (compBuf == NULL) { + fprintf(stderr, "Couldn't allocate %s key buffer.\n", label); + return -1; + } + compSz = loadFn(compBuf, &compBufSz); + if (compSz == 0) { + 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) { + WFREE(compBuf, NULL, 0); + fprintf(stderr, "Couldn't use %s key buffer.\n", label); + return -1; + } + WFREE(compBuf, NULL, 0); + return 0; +} + +typedef struct { + const char* substr; + int (*loadFn)(byte*, word32*); + const char* label; +} MlDsaCompositeEntry; + +/* NULL-terminated so the table stays non-empty (a portable initializer list + * can't otherwise omit every entry) when a build enables neither ECDSA nor + * Ed25519/Ed448. */ +static const MlDsaCompositeEntry mldsaCompositeEntries[] = { +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + { "mldsa44-ed25519", load_key_mldsa44_ed25519, "ML-DSA-44+Ed25519" }, +#endif + { NULL, NULL, NULL } +}; + static int LoadMlDsaHostKeys(WOLFSSH_CTX* ctx, const char* keyList) { byte* mldsaBuf; @@ -3302,12 +3368,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; + + /* LoadMlDsaHostKeys() only understands the plain "mldsa-NN" + * names; skip it for a purely composite keyList (e.g. + * "mldsa44-ed25519") since it would otherwise report no + * supported level matched and abort loading the composite + * keys below. */ + 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 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->loadFn, + entry->label) != 0) { + #ifdef WOLFSSH_SMALL_STACK + WFREE(keyLoadBuf, NULL, 0); + #endif + ES_ERROR("Error loading %s host key.\n", + entry->label); + } + } + } + + if (!mldsaMatched) { + #ifdef WOLFSSH_SMALL_STACK + 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 0a17590d5..c86e289c4 100644 --- a/keys/include.am +++ b/keys/include.am @@ -26,5 +26,5 @@ EXTRA_DIST+= \ 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-mldsa87.der keys/server-key-mldsa44ed25519 diff --git a/keys/server-key-mldsa44ed25519 b/keys/server-key-mldsa44ed25519 new file mode 100644 index 0000000000000000000000000000000000000000..ead7eed17025f24c5de469fc1872133ecccd09b0 GIT binary patch literal 2894 zcmeI!`BM@I6bEo6HIL1ZjLP*`6;oMV%Y#xI13bVZEj7)nym2MND-RYmk316%v_Lz+ zH1D$U!h^`{Knu{!TeCyW3sWo4fVJIU_8-{ad^2y}%$x7LA3yVO;X#+8qC)hsLGk)A zN5!^F;u7vs(0}I7ow%5oBxZZ6{fvfP42<$KHr5XcG&C_eY6{2wd*Y7(T-bIDE?A{> zSbooxoc+SN6|Kk%KhK?(y**NEYX7Fg$p(6)a%5|)GIRqG2|gdpn~KtGUND%HW#{x_ zp9EpszRY;@uv%#~%5DsfM06Y~ND#VN?+HmHqwn`QaZ?)U$2qVIgm0-?fbt76N5ka+ zeuFndQ@!M60ZAW_*E{oPX&Ah&qL7wXoz$T7bn&_zI&+i`aIt)nT5D~hfu$b^K#^G& z4C7yDPbTJ5TRvd`UIT^jnlPonw=#6rg2Hbaj2F7>1~SmCddapvLJlIEmbup$3{vON z=HN}li8NHZ1AS;yg>omhH~&lXF1A&df-*_o&2zus2hObjK#Xoy`Xb+{g0kv6hG_Pv z!wt8URa}{tdf`m#1657yW!oCnEOuMR>vst;>ux$CP`>M4e0FPj9I_V=NB$}Fc4 zYwo|yTHP2sUv?*x?(P$?0?Anvy!tArW9*;e?#iEQvSE0m!`=i}S(=36o59SQaezA$ z)R^*BDw4`P@Or*o*K`_&A8NFKeNChl{&gl=+MyNrHGSqHIoY&uijQWal{ReG*7_@}w1dAxo=YnP7_PSVA8y$kS93N-Fdj^oY#E7^+dMJ0 ze#iqXQmDSgP70y3m)7hG0i%ke%bjOTjJK|}y!js_n z)~Z*x{C%llNA84~)V&mF(wvd%QI%nTB#1(GjUr&2>dZI&m(&~ZOsQO?Jdc+K!;420 z7bOyxX4+5T0wtZtPX|Q)WdM)lf<-Bx$M5dmli)tULWP_WUd}QC>rvPUGst~Jd!$TT z#fA@{b~OGB%VRaA(^}hg)Vr{o6l!VHIphBHUay9Iw^4AGd8*1l4`qX6ocVoj95`IqUtUn4qY+g@w$)G$zC-HgF zlTa!0y74 zllBH7&tOQ&%Rl;@pTXV<29}Q97~CT0=o!-y3j|h;+kIHXo=4$B*Ya_4q#qA4Y>Pdm zQAkJ6b@c9JQOexxV_AZ*SJAsF?lpft<6@aarAmo>!IxSuJMvtujW$)}yna{Q6-abC zIiVUjCyJHA#d~#fyVJZ~Uxq2gd3{J4V}t)}D2fG%D&ON*;9N3HJRfw{t{UJ1y@lf$ zHa1;(lN?|SjBHRaDm{0i1*&QiMe?iWlrAS&yJFHg4keyId == ID_X509V3_MLDSA87) { wc_MlDsaKey_Free(&key->ks.mldsa.key); } + else if (key->keyId == ID_MLDSA44_ED25519) { + CompositeParams params; + if (WS_GetCompositeParams(key->keyId, ¶ms) == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + wc_MlDsaKey_Free(&key->ks.mldsa_composite.mldsa); + 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 || @@ -2193,6 +2206,66 @@ static int GetOpenSshKeyPublicMlDsa(MlDsaKey* key, const byte* buf, } return ret; } + +/* Public-key-only counterpart to GetOpenSshKeyMlDsaComposite(): a + * public-key blob has no trailing private-key string, so it must not be + * parsed with the private-key decoder (which unconditionally reads a + * second string for the private seed material). */ +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 = 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; + } + } + + 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); + } + ret = WS_CRYPTO_FAILED; + } + return ret; +} #endif #ifndef WOLFSSH_NO_RSA static int GetOpenSshPublicKeyRsa(RsaKey* key, const byte* buf, word32 len, @@ -2259,6 +2332,14 @@ static int GetOpenSshPublicKey(WS_KeySignature *key, case ID_ED25519: ret = GetOpenSshKeyPublicEd25519(&key->ks.ed25519.key, buf, len, idx); break; + #endif + #ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ED25519: + 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; @@ -2269,6 +2350,11 @@ static int GetOpenSshPublicKey(WS_KeySignature *key, #endif /* WOLFSSH_TPM */ +#ifndef WOLFSSH_NO_MLDSA +static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, void* tradKey, + void* heap, const byte* buf, word32 len, word32* idx); +#endif + /* * Decodes an OpenSSH format key. */ @@ -2362,16 +2448,25 @@ static int GetOpenSshKey(WS_KeySignature *key, ret = GetOpenSshKeyMlDsa( &key->ks.mldsa.key, str, strSz, &subIdx, WC_ML_DSA_44); + /* GetOpenSshKeyMlDsa() frees its own key + * material on failure; clear keyId so the + * caller's cleanup doesn't free it again. */ + 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; #endif #ifndef WOLFSSH_NO_ED25519 @@ -2379,6 +2474,22 @@ static int GetOpenSshKey(WS_KeySignature *key, ret = GetOpenSshKeyEd25519(&key->ks.ed25519.key, str, strSz, &subIdx); break; + #endif + #ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ED25519: + ret = GetOpenSshKeyMlDsaComposite( + key->keyId, + &key->ks.mldsa_composite.mldsa, + &key->ks.mldsa_composite.trad, + key->heap, + str, strSz, &subIdx); + /* GetOpenSshKeyMlDsaComposite() frees its + * own key material on failure; clear keyId + * so the caller's cleanup doesn't free it + * again. */ + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; + break; #endif default: ret = WS_UNIMPLEMENTED_E; @@ -2403,7 +2514,11 @@ static int GetOpenSshKey(WS_KeySignature *key, check1 <= check2; check1++, subIdx++) { if (check1 != str[subIdx]) { - /* Bad pad value. */ + /* Bad pad value. The per-type decode + * above already succeeded, so free the + * key material it allocated. */ + wolfSSH_KEY_clean(key); + key->keyId = ID_NONE; ret = WS_KEY_FORMAT_E; break; } @@ -2937,6 +3052,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; @@ -2946,6 +3064,56 @@ 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; + /* OpenSSH private key files are PEM-armored. Strip the PEM wrapper + * when present so that IdentifyOpenSshKey receives the raw binary + * (the openssh-key-v1 magic) rather than the base64 text. The + * generic wc_KeyPemToDer() does not recognize the + * "OPENSSH PRIVATE KEY" header, so the markers are located and the + * body base64-decoded directly, mirroring DoOpenSshKey() in + * src/ssh.c. */ + if (inSz >= 5 && WMEMCMP(in, "-----", 5) == 0) { + static const char* beginMarker = + "-----BEGIN OPENSSH PRIVATE KEY-----"; + static const char* endMarker = + "-----END OPENSSH PRIVATE KEY-----"; + word32 beginSz = (word32)WSTRLEN(beginMarker); + const char* footer; + const byte* b64; + word32 b64Sz; + word32 derOutSz = inSz; + + if (inSz <= beginSz || WMEMCMP(in, beginMarker, beginSz) != 0) { + WFREE(der, heap, dynamicType); + return WS_BAD_FILE_E; + } + + footer = WSTRNSTR((const char*)in + beginSz, endMarker, + inSz - beginSz); + if (footer == NULL) { + WFREE(der, heap, dynamicType); + return WS_BAD_FILE_E; + } + + b64 = in + beginSz; + b64Sz = (word32)(footer - (const char*)b64); + + ret = Base64_Decode(b64, b64Sz, der, &derOutSz); + if (ret != 0) { + ForceZero(der, inSz); + WFREE(der, heap, dynamicType); + return WS_BAD_FILE_E; + } + derSz = derOutSz; + } + else { + WMEMCPY(der, in, inSz); + derSz = inSz; + } + } #ifdef WOLFSSH_CERTS else if (format == WOLFSSH_FORMAT_PEM) { /* The der size will be smaller than the pem size. */ @@ -2976,7 +3144,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) { ForceZero(der, derSz); @@ -3359,6 +3530,9 @@ static const NameIdPair NameIdMap[] = { #ifndef WOLFSSH_NO_MLDSA44 { ID_MLDSA44, TYPE_KEY, "ssh-mldsa-44" }, #endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + { ID_MLDSA44_ED25519, TYPE_KEY, "ssh-mldsa44-ed25519@openssh.com" }, +#endif #ifndef WOLFSSH_NO_MLDSA65 { ID_MLDSA65, TYPE_KEY, "ssh-mldsa-65" }, #endif @@ -5592,7 +5766,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 @@ -5614,6 +5790,37 @@ struct wolfSSH_sigKeyBlock { struct { ed25519_key key; } ed25519; +#endif +#ifndef WOLFSSH_NO_MLDSA + struct { + 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) + /* No traditional algorithm is available to pair with + * ML-DSA in this build. Keep the union non-empty (a + * standards-compliant empty union is rejected by some + * compilers) even though composite keys are unusable + * without a traditional component. */ + byte placeholder; +#endif + } trad; + /* WC_MLDSA_87_PUB_KEY_SIZE is the largest mldsaPubSz and + * COMPOSITE_MAX_TRAD_PUB_SZ the largest tradPubSz across all + * combos in WS_GetCompositeParams(); keep both 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; }; @@ -5805,6 +6012,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, @@ -6194,6 +6425,13 @@ static int ParsePubKey(WOLFSSH *ssh, break; #endif #endif +#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ED25519: + sigKeyBlock_ptr->useMlDsaComposite = 1; + sigKeyBlock_ptr->pubKeyId = ssh->handshake->pubKeyId; + ret = ParseMlDsaCompositePubKey(ssh, sigKeyBlock_ptr, pubKey, pubKeySz, ssh->handshake->pubKeyId); + break; +#endif default: ret = WS_INVALID_ALGO_ID; @@ -6260,6 +6498,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; + if (WS_GetCompositeParams(p->pubKeyId, ¶ms) == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + wc_MlDsaKey_Free(&p->sk.mldsa_composite.mldsa); + if (ops != NULL) { + ops->free(&p->sk.mldsa_composite.trad); + } + } + } +#endif p->keyAllocated = 0; } } @@ -7143,6 +7393,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.mldsa, + &sigKeyBlock_ptr->sk.mldsa_composite.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; } @@ -8448,7 +8712,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. */ @@ -9381,6 +9644,12 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData, ret = DoUserAuthRequestMlDsa(ssh, &authData->sf.publicKey, authData, (byte)mlLevel, 1, pubKeyBlobSz); } +#endif +#ifndef WOLFSSH_NO_MLDSA + else if (pkTypeId == ID_MLDSA44_ED25519) { + ret = DoUserAuthRequestMlDsaComposite(ssh, &authData->sf.publicKey, + authData, pkTypeId, pubKeyBlobSz); + } #endif else { wc_HashAlg hash; @@ -12626,6 +12895,37 @@ struct wolfSSH_sigKeyBlockFull { #endif word32 qSz; } mldsa; +#endif +#ifndef WOLFSSH_NO_MLDSA + struct { + 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) + /* No traditional algorithm is available to pair with + * ML-DSA in this build. Keep the union non-empty (a + * standards-compliant empty union is rejected by some + * compilers) even though composite keys are unusable + * without a traditional component. */ + byte placeholder; +#endif + } trad; + /* WC_MLDSA_87_PUB_KEY_SIZE is the largest mldsaPubSz and + * COMPOSITE_MAX_TRAD_PUB_SZ the largest tradPubSz across all + * combos in WS_GetCompositeParams(); keep both 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; }; @@ -12646,7 +12946,9 @@ struct wolfSSH_sigKeyBlockFull { #ifdef WOLFSSH_NO_MLDSA #define KEX_SIG_SIZE (512) #else - #define KEX_SIG_SIZE MLDSA_MAX_SIG_SIZE + /* +114 covers the largest trad signature appended by + * SignHMlDsaComposite(): Ed448, bigger than Ed25519 (64) or P-384 (106). */ + #define KEX_SIG_SIZE (MLDSA_MAX_SIG_SIZE + 114) #endif #ifdef WOLFSSH_CERTS @@ -13185,6 +13487,107 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, break; } #endif /* WOLFSSH_NO_MLDSA */ +#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ED25519: + { + 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 OpenSSH-key-v1 envelope, + * not just the key data; must go through GetOpenSshKey() to + * walk it (calling GetOpenSshKeyMlDsaComposite() directly + * would misparse the envelope magic as a string length). */ + { + 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) { + /* Ownership of keySig's key material is transferred + * to sigKeyBlock_ptr here (shallow copy, not a + * duplicate) -- keySig must not be freed via + * wolfSSH_KEY_clean() or otherwise reused past this + * point. */ + sigKeyBlock_ptr->sk.mldsa_composite.mldsa = + keySig.ks.mldsa_composite.mldsa; + XMEMCPY(&sigKeyBlock_ptr->sk.mldsa_composite.trad, + &keySig.ks.mldsa_composite.trad, + sizeof(sigKeyBlock_ptr->sk.mldsa_composite.trad)); + } + } + + if (ret == 0) { + word32 mldsaPubSz = params.mldsaPubSz; + ret = wc_MlDsaKey_ExportPubRaw( + &sigKeyBlock_ptr->sk.mldsa_composite.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.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; @@ -14317,6 +14720,11 @@ static int SignH(WOLFSSH* ssh, byte* sig, word32* sigSz, case ID_X509V3_MLDSA87: ret = SignHMlDsa(ssh, sig, sigSz, sigKey); break; +#endif +#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ED25519: + ret = SignHMlDsaComposite(ssh, sig, sigSz, sigKey); + break; #endif default: ret = WS_INVALID_ALGO_ID; @@ -14649,6 +15057,17 @@ int SendKexDhReply(WOLFSSH* ssh) ) { wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa.key); } + else if (sigKeyBlock_ptr->pubKeyId == ID_MLDSA44_ED25519) { + CompositeParams params; + if (WS_GetCompositeParams(sigKeyBlock_ptr->pubKeyId, ¶ms) + == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.mldsa); + if (ops != NULL) { + ops->free(&sigKeyBlock_ptr->sk.mldsa_composite.trad); + } + } + } #endif } @@ -14788,6 +15207,18 @@ int SendKexDhReply(WOLFSSH* ssh) idx += sigKeyBlock_ptr->sk.mldsa.qSz; } break; + + case ID_MLDSA44_ED25519: + { + /* Copy the composite key block (ML-DSA public key followed by + * the traditional public key) into the buffer. */ + 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 @@ -17479,6 +17910,12 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, break; #endif #endif +#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ED25519: + ret = PrepareUserAuthRequestMlDsaComposite(ssh, + payloadSz, authData, keySig); + break; +#endif default: ret = WS_INVALID_ALGO_ID; } @@ -17640,6 +18077,21 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, break; #endif #endif +#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ED25519: + 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; } @@ -19897,7 +20349,1061 @@ 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_ED25519) + 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 + default: + return WS_BAD_ARGUMENT; + } + + /* COMPOSITE_MAX_LABEL_SZ sizes the m_prime stack buffers in + * VerifyMlDsaComposite()/SignHMlDsaComposite(); a label added above + * without updating that constant would silently overflow those + * buffers. Fail loudly instead. */ + 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); + } + } + return ret; + } +#endif + return wc_Hash(hashId, msg, msgSz, hash, hashSz); +} + +/* CompositeTradOps: one instance per traditional algorithm, replacing the + * tradType if/else-if chain that used to be hand-copied at every composite + * key call site. See CompositeTradOps in wolfssh/internal.h. */ + +#ifndef WOLFSSH_NO_ECDSA +static int CompositeEccInit(void* key, void* heap) +{ + return wc_ecc_init_ex((ecc_key*)key, heap, INVALID_DEVID); +} + +static void CompositeEccFree(void* key) +{ + wc_ecc_free((ecc_key*)key); +} + +static int CompositeEccImportPub(void* key, const byte* pub, word32 pubSz) +{ + return wc_ecc_import_x963(pub, pubSz, (ecc_key*)key); +} + +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); +} + +static int CompositeEccExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ecc_export_x963((ecc_key*)key, out, outSz); +} + +static int CompositeEccSign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* m_prime, word32 m_primeLen, + byte* wireSig, word32* wireSigSz) +{ + int ret; + byte asnSig[ECDSA_ASN_SIG_SZ]; + word32 asnSigSz = sizeof(asnSig); + byte digest[WC_MAX_DIGEST_SIZE]; + + (void)heap; + + ret = WS_Hash_Helper(tradHashId, m_prime, m_primeLen, 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; + byte rBuf[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; + byte sBuf[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; + + ret = wc_ecc_sig_to_rs(asnSig, asnSigSz, rBuf, &rSz, sBuf, &sSz); + if (ret == 0) { + word32 offset = 0; + + if (*wireSigSz < (2U * LENGTH_SZ) + rSz + sSz) { + return WS_BAD_ARGUMENT; + } + + c32toa(rSz, wireSig + offset); + offset += LENGTH_SZ; + WMEMCPY(wireSig + offset, rBuf, rSz); + offset += rSz; + + c32toa(sSz, wireSig + offset); + offset += LENGTH_SZ; + WMEMCPY(wireSig + offset, sBuf, sSz); + offset += sSz; + + *wireSigSz = offset; + } + } + if (ret != 0) { + ret = WS_ECC_E; + } + + return ret; +} + +static int CompositeEccVerify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* m_prime, word32 m_primeLen) +{ + int ret; + const byte* r = NULL; + const byte* s = NULL; + word32 rSz = 0, sSz = 0; + word32 i = 0; + byte asnSig[ECDSA_ASN_SIG_SZ]; + word32 asnSigSz = ECDSA_ASN_SIG_SZ; + + (void)heap; + + ret = GetStringRef(&rSz, &r, wireSig, wireSigSz, &i); + if (ret == WS_SUCCESS) { + ret = GetStringRef(&sSz, &s, wireSig, wireSigSz, &i); + } + 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, m_prime, m_primeLen, 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; + } + } + + return ret; +} + +static const CompositeTradOps compositeEccOps = { + TRAD_TYPE_ECC, + CompositeEccInit, CompositeEccFree, + CompositeEccImportPub, CompositeEccImportPriv, CompositeEccExportPub, + CompositeEccSign, CompositeEccVerify +}; +#endif /* !WOLFSSH_NO_ECDSA */ + +#ifndef WOLFSSH_NO_ED25519 +static int CompositeEd25519Init(void* key, void* heap) +{ + return wc_ed25519_init_ex((ed25519_key*)key, heap, INVALID_DEVID); +} + +static void CompositeEd25519Free(void* key) +{ + wc_ed25519_free((ed25519_key*)key); +} + +static int CompositeEd25519ImportPub(void* key, const byte* pub, word32 pubSz) +{ + return wc_ed25519_import_public(pub, pubSz, (ed25519_key*)key); +} + +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); +} + +static int CompositeEd25519ExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ed25519_export_public((ed25519_key*)key, out, outSz); +} + +static int CompositeEd25519Sign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* m_prime, word32 m_primeLen, + 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(m_prime, m_primeLen, wireSig, &sigSz, + (ed25519_key*)key); + if (ret != 0 || sigSz != ED25519_SIG_SIZE) { + ret = WS_ED25519_E; + } + else { + *wireSigSz = sigSz; + } + + return ret; +} + +static int CompositeEd25519Verify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* m_prime, word32 m_primeLen) +{ + int ret; + int res = 0; + + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + ret = wc_ed25519_verify_msg(wireSig, wireSigSz, m_prime, m_primeLen, + &res, (ed25519_key*)key); + if (ret != 0 || res != 1) { + ret = WS_ED25519_E; + } + + return ret; +} + +static const CompositeTradOps compositeEd25519Ops = { + TRAD_TYPE_ED25519, + CompositeEd25519Init, CompositeEd25519Free, + CompositeEd25519ImportPub, CompositeEd25519ImportPriv, + CompositeEd25519ExportPub, + CompositeEd25519Sign, CompositeEd25519Verify +}; +#endif /* !WOLFSSH_NO_ED25519 */ + +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 + default: + return NULL; + } +} + +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; + byte hash[WC_MAX_DIGEST_SIZE]; + byte m_prime[COMPOSITE_DOMAIN_PREFIX_SZ + COMPOSITE_MAX_LABEL_SZ + 1 + + WC_MAX_DIGEST_SIZE]; + word32 m_prime_len = 0; + int status = 0; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + if (params.tradHashSz > WC_MAX_DIGEST_SIZE) { + return WS_BUFFER_E; + } + + /* sig/sigSz are wire-controlled; without this check, "sigSz - + * mldsaSigSz" below could underflow to a huge value. */ + if (sigSz < params.mldsaSigSz) { + return WS_KEY_FORMAT_E; + } + + /* ED25519/ED448 signatures are fixed size so an exact match works; + * ECC's variable-length r/s encoding is bounds-checked incrementally + * via GetStringRef() below instead. */ + if (params.tradType == TRAD_TYPE_ED25519 || params.tradType == TRAD_TYPE_ED448) { + if (sigSz != (params.mldsaSigSz + params.tradSigSz)) { + return WS_KEY_FORMAT_E; + } + } + + m_prime_len = 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) { + XMEMCPY(m_prime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + XMEMCPY(m_prime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); + m_prime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; + XMEMCPY(m_prime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + } + + /* Check the cheap trad signature before the expensive ML-DSA verify, + * so a garbage/tampered signature is rejected cheaply -- matters on + * pre-auth KEX and unauthenticated user-auth probing. */ + 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, m_prime, m_prime_len); + } + } + + if (ret == WS_SUCCESS) { + ret = wc_MlDsaKey_VerifyCtx(mldsa, + sig, params.mldsaSigSz, + (const byte*)params.label, (byte)params.labelSz, + m_prime, m_prime_len, + &status); + if (ret != 0 || status != 1) { + WLOG(WS_LOG_DEBUG, "VerifyMlDsaComposite: ML-DSA Verify fail (%d, status=%d)", ret, status); + ret = WS_MLDSA_E; + } + } + + ForceZero(hash, sizeof(hash)); + ForceZero(m_prime, sizeof(m_prime)); + + return ret; +} + +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; + CompositeParams params; + const CompositeTradOps* ops; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + ops = WS_GetTradOps(params.tradType); + + /* GetOpenSshKey() does not clean up on our error, so every failure + * path below must free whichever of mldsa/tradKey it already + * initialized itself (mirrors GetOpenSshKeyMlDsa()). */ + ret = wc_MlDsaKey_Init(mldsa, heap, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(mldsa, params.mldsaLevel); + } + if (ret != 0) { + wc_MlDsaKey_Free(mldsa); + return WS_CRYPTO_FAILED; + } + if (ops == NULL) { + wc_MlDsaKey_Free(mldsa); + return WS_UNIMPLEMENTED_E; + } + ret = ops->init(tradKey, heap); + if (ret != 0) { + wc_MlDsaKey_Free(mldsa); + return 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; +} + +static int ParseMlDsaCompositePubKey(WOLFSSH* ssh, + struct wolfSSH_sigKeyBlock* sigKeyBlock_ptr, + byte* pubKey, word32 pubKeySz, byte keyId) +{ + int ret; + 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 = wc_MlDsaKey_Init(&sigKeyBlock_ptr->sk.mldsa_composite.mldsa, + ssh->ctx->heap, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(&sigKeyBlock_ptr->sk.mldsa_composite.mldsa, params.mldsaLevel); + if (ret != 0) { + wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.mldsa); + return WS_INVALID_ALGO_ID; + } + } + else { + return WS_INVALID_ALGO_ID; + } + + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->init(&sigKeyBlock_ptr->sk.mldsa_composite.trad, ssh->ctx->heap); + } + + if (ret != 0) { + wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.mldsa); + return WS_INVALID_ALGO_ID; + } + + 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.mldsa, + pub, params.mldsaPubSz); + if (ret == WS_SUCCESS) { + ret = ops->importPub(&sigKeyBlock_ptr->sk.mldsa_composite.trad, + pub + params.mldsaPubSz, params.tradPubSz); + } + + if (ret == 0) { + sigKeyBlock_ptr->keyAllocated = 1; + } + else { + wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.mldsa); + ops->free(&sigKeyBlock_ptr->sk.mldsa_composite.trad); + ret = WS_INVALID_ALGO_ID; + } + return ret; +} + +static int SignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, + struct wolfSSH_sigKeyBlockFull *sigKey) +{ + int ret; + CompositeParams params; + byte hash[WC_MAX_DIGEST_SIZE]; + byte m_prime[COMPOSITE_DOMAIN_PREFIX_SZ + COMPOSITE_MAX_LABEL_SZ + 1 + + WC_MAX_DIGEST_SIZE]; + word32 m_prime_len = 0; + word32 mldsaSigSz; + byte keyId = sigKey->pubKeyId; + + 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; + + /* All trad types (ED25519, ED448, ECC) write into the same caller + * supplied sig buffer immediately after the ML-DSA component; verify + * the buffer is large enough for the worst case before signing. */ + if (*sigSz < (params.mldsaSigSz + params.tradSigSz)) { + return WS_BAD_ARGUMENT; + } + + m_prime_len = 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) { + XMEMCPY(m_prime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + XMEMCPY(m_prime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); + m_prime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; + XMEMCPY(m_prime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + } + + if (ret == WS_SUCCESS) { + ret = wc_MlDsaKey_SignCtx(&sigKey->sk.mldsa_composite.mldsa, + (const byte*)params.label, + params.labelSz, + sig, &mldsaSigSz, m_prime, m_prime_len, 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.trad, ssh->rng, + ssh->ctx->heap, params.tradHashId, params.tradHashSz, + m_prime, m_prime_len, sig + params.mldsaSigSz, &wireSigSz); + if (ret == WS_SUCCESS) { + *sigSz = params.mldsaSigSz + wireSigSz; + } + else { + WLOG(WS_LOG_DEBUG, "SignHMlDsaComposite: trad sign fail (%d)", ret); + } + } + } + + ForceZero(hash, sizeof(hash)); + ForceZero(m_prime, sizeof(m_prime)); + + 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 = keySig->keyId; + + WLOG(WS_LOG_DEBUG, "Entering PrepareUserAuthRequestMlDsaComposite()"); + if (ssh == NULL || payloadSz == NULL || authData == NULL || keySig == NULL) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS) { + ret = WS_GetCompositeParams(keyId, ¶ms); + } + + if (ret == WS_SUCCESS) { + word32 idx = 0; + + /* Composite private keys only exist in OpenSSH format (seed-based + * keys have no ASN.1 representation), so GetOpenSshKey() must be + * used here; it also self-initializes keySig's composite key + * members, so no pre-init is 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* m_prime = NULL; + word32 mldsaSigSz; + word32 m_prime_len; + CompositeParams params; + byte keyId = keySig->keyId; + + WLOG(WS_LOG_DEBUG, "Entering BuildUserAuthRequestMlDsaComposite()"); + if (ssh == NULL || output == NULL || idx == NULL || authData == NULL || + sigStart == NULL || keySig == NULL) { + return WS_BAD_ARGUMENT; + } + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + mldsaSigSz = params.mldsaSigSz; + sigSz = (word32)keySig->sigSz; + + /* keySig->sigSz (set in PrepareUserAuthRequestMlDsaComposite) is + * already params.mldsaSigSz + params.tradSigSz, the worst-case total; + * the extra 32 bytes is just defensive slack, not load-bearing. */ + sig = (byte*)WMALLOC(sigSz + 32, 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); + m_prime_len = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + params.tradHashSz; + m_prime = (byte*)WMALLOC(m_prime_len, keySig->heap, DYNTYPE_TEMP); + + if (hash == NULL || m_prime == 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) { + XMEMCPY(m_prime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + XMEMCPY(m_prime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); + m_prime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; + XMEMCPY(m_prime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + } + + 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, m_prime, m_prime_len, 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, + m_prime, m_prime_len, 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) { + ForceZero(sig, sigSz); + WFREE(sig, keySig->heap, DYNTYPE_BUFFER); + } + if (checkData != NULL) { + ForceZero(checkData, checkDataSz); + WFREE(checkData, keySig->heap, DYNTYPE_TEMP); + } + if (hash != NULL) { + ForceZero(hash, params.tradHashSz); + WFREE(hash, keySig->heap, DYNTYPE_TEMP); + } + if (m_prime != NULL) { + ForceZero(m_prime, m_prime_len); + WFREE(m_prime, 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 = wc_MlDsaKey_Init(&keySig->ks.mldsa_composite.mldsa, keySig->heap, INVALID_DEVID); + if (ret == 0) { + mldsaInit = 1; + ret = wc_MlDsaKey_SetParams(&keySig->ks.mldsa_composite.mldsa, params.mldsaLevel); + } + if (ret == 0) { + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->init(&keySig->ks.mldsa_composite.trad, keySig->heap); + if (ret == 0) tradInit = 1; + } + } + + 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); + + 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 the KEX host-key signing path (SignHMlDsaComposite) directly, + * the same function SendKexDhReply() calls through SignH(). Builds a + * throwaway composite keypair rather than requiring a loaded host key, so + * callers only need ssh->rng and ssh->h/hSz populated. */ +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.mldsa, + ssh->ctx->heap, INVALID_DEVID); + if (ret == 0) + ret = wc_MlDsaKey_SetParams(&sigKey.sk.mldsa_composite.mldsa, + params.mldsaLevel); + if (ret == 0) + ret = wc_MlDsaKey_MakeKey(&sigKey.sk.mldsa_composite.mldsa, ssh->rng); + if (ret != 0) { + wc_MlDsaKey_Free(&sigKey.sk.mldsa_composite.mldsa); + return WS_CRYPTO_FAILED; + } + + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->init(&sigKey.sk.mldsa_composite.trad, ssh->ctx->heap); + /* make_key isn't part of CompositeTradOps -- it's only needed here, + * to build a throwaway test keypair, so a small local branch is + * cheaper than an 8th ops pointer used by exactly one call site. */ + 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.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.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.trad.ecc); +#endif + } + } + } + + if (ret == 0) { + ret = SignHMlDsaComposite(ssh, sig, sigSz, &sigKey); + } + + wc_MlDsaKey_Free(&sigKey.sk.mldsa_composite.mldsa); + if (ops != NULL) { + ops->free(&sigKey.sk.mldsa_composite.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..719a19107 100644 --- a/src/keygen.c +++ b/src/keygen.c @@ -47,7 +47,15 @@ #ifndef WOLFSSH_NO_ECDSA #include #endif +#ifndef WOLFSSH_NO_ED25519 + #include +#endif +#ifdef HAVE_ED448 + #include +#endif #include +#include +#include #ifdef WOLFSSH_KEYGEN @@ -330,6 +338,330 @@ int wolfSSH_MakeMlDsaKey(byte* out, word32 outSz, word32 level) #endif } + +/* Composite private keys have no ASN.1 representation (the traditional + * component is stored seed/scalar-first, per the composite draft's + * seed-based KeyGen_internal), so the only on-disk form is the OpenSSH-key-v1 + * envelope that GetOpenSshKey()/GetOpenSshKeyMlDsaComposite() in + * src/internal.c already know how to parse. This builds that same 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 }, padding } + * where pub is (mldsaPub || tradPub) and priv is (mldsaSeed || tradPriv). */ +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; +#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 + 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 (out == NULL) { + return WS_BAD_ARGUMENT; + } + + if (level == WOLFSSH_MLDSAKEY_44 && + tradType == WOLFSSH_COMPOSITE_TRAD_ED25519) + keyId = ID_MLDSA44_ED25519; + 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); + + /* Compute sizes needed for the composite key blob. The final + * base64-encoded size is only known (and checked against outSz) once + * the PEM body has been built below, after key generation. */ + 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; + + 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 && params.tradType == TRAD_TYPE_ED25519) { +#ifndef WOLFSSH_NO_ED25519 + if (wc_ed25519_init(&ed25519Key) != 0) { + ret = WS_CRYPTO_FAILED; + } + else { + ed25519Init = 1; + if (wc_ed25519_make_key(&rng, ED25519_KEY_SIZE, &ed25519Key) != 0) + ret = WS_CRYPTO_FAILED; + } + if (ret == 0) { + sz = params.tradPrivSz; + if (wc_ed25519_export_private_only(&ed25519Key, tradPriv, &sz) + != 0 || sz != params.tradPrivSz) + ret = WS_CRYPTO_FAILED; + } + if (ret == 0) { + sz = params.tradPubSz; + if (wc_ed25519_export_public(&ed25519Key, tradPub, &sz) != 0 || + sz != params.tradPubSz) + ret = WS_CRYPTO_FAILED; + } +#else + ret = WS_NOT_COMPILED; +#endif + } + else if (ret == 0 && params.tradType == TRAD_TYPE_ED448) { +#ifdef HAVE_ED448 + if (wc_ed448_init(&ed448Key) != 0) { + ret = WS_CRYPTO_FAILED; + } + else { + ed448Init = 1; + if (wc_ed448_make_key(&rng, ED448_KEY_SIZE, &ed448Key) != 0) + ret = WS_CRYPTO_FAILED; + } + if (ret == 0) { + sz = params.tradPrivSz; + if (wc_ed448_export_private_only(&ed448Key, tradPriv, &sz) != 0 || + sz != params.tradPrivSz) + ret = WS_CRYPTO_FAILED; + } + if (ret == 0) { + sz = params.tradPubSz; + if (wc_ed448_export_public(&ed448Key, tradPub, &sz) != 0 || + sz != params.tradPubSz) + ret = WS_CRYPTO_FAILED; + } +#else + ret = WS_NOT_COMPILED; +#endif + } + else if (ret == 0 && params.tradType == TRAD_TYPE_ECC) { +#ifndef WOLFSSH_NO_ECDSA + int eccKeySz = (int)params.tradPrivSz; + + if (wc_ecc_init(&eccKey) != 0) { + ret = WS_CRYPTO_FAILED; + } + else { + eccInit = 1; + if (wc_ecc_make_key(&rng, eccKeySz, &eccKey) != 0) + ret = WS_CRYPTO_FAILED; + } + if (ret == 0) { + sz = params.tradPrivSz; + if (wc_ecc_export_private_only(&eccKey, tradPriv, &sz) != 0 || + sz != params.tradPrivSz) + ret = WS_CRYPTO_FAILED; + } + if (ret == 0) { + sz = params.tradPubSz; + if (wc_ecc_export_x963(&eccKey, tradPub, &sz) != 0 || + sz != params.tradPubSz) + ret = WS_CRYPTO_FAILED; + } +#else + ret = WS_NOT_COMPILED; +#endif + } + 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 { + int b64Ret = Base64_Encode(tmpBuf, fileSz, NULL, &b64Sz); + if (b64Ret != 0 && b64Ret != LENGTH_ONLY_E) { + ret = WS_CRYPTO_FAILED; + } + else if (outSz < b64Sz + WSTRLEN(header) + WSTRLEN(footer)) { + WLOG(WS_LOG_DEBUG, "Output buffer too small for composite key"); + ret = WS_BUFFER_E; + } + else { + 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); + ret = (int)off; + } + else ret = WS_CRYPTO_FAILED; + } + } + } + + if (mldsaInit) wc_MlDsaKey_Free(&mldsaKey); +#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); + + ForceZero(mldsaSeed, sizeof(mldsaSeed)); + ForceZero(tradPriv, sizeof(tradPriv)); + + if (tmpBuf != NULL) { + 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/tests/kex.c b/tests/kex.c index b08560cf7..1059f15a8 100644 --- a/tests/kex.c +++ b/tests/kex.c @@ -515,6 +515,13 @@ 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) + /* 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 AssertIntEQ(wolfSSH_Cleanup(), WS_SUCCESS); diff --git a/tests/unit.c b/tests/unit.c index 6be8faed1..e1955b8ff 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -836,6 +836,17 @@ static int test_MlDsaKeyGen(void) result = -106; 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; + } sz = wolfSSH_MakeMlDsaKey(der, params[i].derSz - 1, params[i].level); if (sz != WS_CRYPTO_FAILED) { @@ -859,6 +870,136 @@ static int test_MlDsaKeyGen(void) return result; } + +/* 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) +{ + static const struct { + word32 level; + word32 tradType; + const char* name; + } params[] = { + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + { WOLFSSH_MLDSAKEY_44, WOLFSSH_COMPOSITE_TRAD_ED25519, "44+Ed25519" }, + #endif + }; + 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])); 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 = -109; + 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 = -109; + 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 = -110; + 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 = -111; + } + + wolfSSH_CTX_free(ctx); + WFREE(buf, NULL, DYNTYPE_BUFFER); + if (result != 0) { + break; + } + } + + if (result == 0) { + int sz = wolfSSH_MakeMlDsaCompositeKey(NULL, 0, firstLevel, + firstTradType); + if (sz != WS_BAD_ARGUMENT) { + printf("MlDsaCompositeKeyGen: NULL out wrong result %d\n", sz); + result = -112; + } + } + + 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 = -113; + } + } + + 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 = -115; + } + } + + /* 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 && (sizeof(params) / sizeof(params[0])) > 0) { + byte* buf = (byte*)WMALLOC(firstSz, NULL, DYNTYPE_BUFFER); + + if (buf == NULL) { + printf("MlDsaCompositeKeyGen: alloc failed for undersized " + "buffer test\n"); + result = -114; + } + 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 = -115; + } + WFREE(buf, NULL, DYNTYPE_BUFFER); + } + } + + return result; +} #endif #endif @@ -4423,6 +4564,845 @@ static int test_DoUserAuthRequestMlDsa(void) return 0; } +static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, + byte keyId, int tamperSig) +{ + 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; + 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; + 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_SERVER, NULL); + if (ctx == NULL) return -700; + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -701; goto done; } + + /* Stub a session id so the verify hash has something to absorb. */ + ssh->sessionIdSz = 16; + WMEMSET(ssh->sessionId, 0xA5, ssh->sessionIdSz); + + if (wc_InitRng(&rng) != 0) { + result = -702; + goto done; + } + rngInit = 1; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) { result = -703; 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 = (int)params.tradPrivSz; + 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 + } + + pubRaw = (byte*)WMALLOC(params.mldsaPubSz, NULL, 0); + if (pubRaw == NULL) { result = -716; goto done; } + { + 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 + } + 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; } + off = 0; + MlDsaTest_PutLen(tradSig + off, rSz); off += UINT32_SZ; + WMEMCPY(tradSig + off, rBuf, rSz); off += rSz; + MlDsaTest_PutLen(tradSig + off, sSz); off += UINT32_SZ; + 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) { + /* 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; + } + + 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; + } + + /* Regression check for the specific historical bug: the old + * KEX_SIG_SIZE only reserved 64 bytes of trad headroom, which is + * insufficient for ECC P-384 (106 bytes) and Ed448 (114 bytes). */ + 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_ED25519) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519); + 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) + 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_ED25519) + 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 + return 0; +} +#endif /* WOLFSSH_KEYGEN */ + +static int test_DoUserAuthRequestMlDsaComposite(void) +{ + int ret = 0; +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + 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; +#endif + return 0; +} + #ifdef WOLFSSH_KEYGEN static int test_PrepareUserAuthRequestMlDsa(void) { @@ -4732,6 +5712,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; @@ -7842,7 +8826,23 @@ 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); @@ -8037,6 +9037,10 @@ 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 diff --git a/wolfssh/internal.h b/wolfssh/internal.h index dba5dbf32..4058732b1 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" @@ -405,6 +408,10 @@ enum { ID_MLDSA44, ID_MLDSA65, ID_MLDSA87, + /* Always declared, even when a sub-algorithm or MLDSA level is + * unavailable; NameIdMap and WS_GetCompositeParams() separately gate + * which of these are actually reachable at runtime. */ + ID_MLDSA44_ED25519, #endif ID_X509V3_SSH_RSA, ID_X509V3_ECDSA_SHA2_NISTP256, @@ -1134,9 +1141,103 @@ typedef struct WS_KeySignature { MlDsaKey key; } mldsa; #endif /* WOLFSSH_NO_MLDSA */ +#ifndef WOLFSSH_NO_MLDSA + struct { + 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) + /* No traditional algorithm is available to pair with + * ML-DSA in this build. Keep the union non-empty (a + * standards-compliant empty union is rejected by some + * compilers) even though composite keys are unusable + * without a traditional component. */ + byte placeholder; +#endif + } trad; + } mldsa_composite; +#endif } 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 traditional public key size across all currently defined + * composite combos: P-384 uncompressed point (1 + 2*ECC_P384_COORD_SZ). */ +#define COMPOSITE_MAX_TRAD_PUB_SZ (1 + (2 * ECC_P384_COORD_SZ)) +/* worst-case traditional private key size across all currently defined + * composite combos: Ed448 raw private key (57 bytes) when available, + * otherwise the P-384 private scalar (ECC_P384_COORD_SZ). */ +#ifdef HAVE_ED448 +#define COMPOSITE_MAX_TRAD_PRIV_SZ ED448_KEY_SIZE +#else +#define COMPOSITE_MAX_TRAD_PRIV_SZ ECC_P384_COORD_SZ +#endif + +typedef struct CompositeParams { + byte keyId; + byte mldsaLevel; + word32 mldsaSigSz; + word32 mldsaPubSz; + byte tradType; + enum wc_HashType tradHashId; + word32 tradHashSz; + const char* label; + word32 labelSz; + word32 tradPubSz; + word32 tradSigSz; + word32 tradPrivSz; +} CompositeParams; + +/* Dispatch table for the traditional (ECC/Ed25519/Ed448) half of a + * composite key, keyed by tradType; see WS_GetTradOps() in src/internal.c. + * init/free/importPub/importPriv/exportPub return raw wc_* codes since + * callers each map failure to a different WS_* code; sign/verify pre-map + * to WS_ED25519_E/WS_ED448_E/WS_ECC_E since that mapping is uniform. */ +typedef struct CompositeTradOps { + byte tradType; + 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* m_prime, word32 m_primeLen, + byte* wireSig, word32* wireSigSz); + int (*verify)(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* m_prime, word32 m_primeLen); +} CompositeTradOps; + +WOLFSSH_LOCAL int WS_GetCompositeParams(byte keyId, CompositeParams* params); +WOLFSSH_LOCAL const CompositeTradOps* WS_GetTradOps(byte tradType); +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); @@ -1542,6 +1643,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..f0031535a 100644 --- a/wolfssh/keygen.h +++ b/wolfssh/keygen.h @@ -46,12 +46,23 @@ extern "C" { #define WOLFSSH_MLDSAKEY_65 65 #define WOLFSSH_MLDSAKEY_87 87 +/* Traditional algorithm paired with the ML-DSA level in a composite key. + * Not every (level, trad) pair is a defined composite: ED25519 pairs with + * 44/65, ED448 pairs with 87 only, ECDSA pairs with all three levels (at + * P-256 for 44/65, P-384 for 87). See WS_GetCompositeParams() in + * src/internal.c for the authoritative 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); +WOLFSSH_API int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, + word32 level, word32 tradType); #ifdef __cplusplus From 0a914b5c5ea48809615af736fc678838420cc85d Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Thu, 16 Jul 2026 14:33:55 -0600 Subject: [PATCH 2/2] Add remaining ML-DSA composite signature algorithms --- apps/wolfsshd/auth.c | 15 ++ apps/wolfsshd/test/test_configuration.c | 15 ++ examples/echoserver/echoserver.c | 80 +++++++ keys/include.am | 5 +- keys/server-key-mldsa44es256 | Bin 0 -> 2937 bytes keys/server-key-mldsa65ed25519 | Bin 0 -> 4154 bytes keys/server-key-mldsa65es256 | Bin 0 -> 4217 bytes keys/server-key-mldsa87ed448 | Bin 0 -> 5505 bytes keys/server-key-mldsa87es384 | Bin 0 -> 5577 bytes src/internal.c | 269 +++++++++++++++++++++++- src/keygen.c | 15 ++ tests/kex.c | 20 ++ tests/unit.c | 97 ++++++++- wolfssh/error.h | 3 +- wolfssh/internal.h | 5 + 15 files changed, 517 insertions(+), 7 deletions(-) create mode 100644 keys/server-key-mldsa44es256 create mode 100644 keys/server-key-mldsa65ed25519 create mode 100644 keys/server-key-mldsa65es256 create mode 100644 keys/server-key-mldsa87ed448 create mode 100644 keys/server-key-mldsa87es384 diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 6346b9afc..d50e3c8ef 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -261,9 +261,24 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, #endif #endif #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + "ssh-mldsa44-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) + "ssh-mldsa65-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSH_NO_ECDSA) + "ssh-mldsa87-es384", + #endif #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) "ssh-mldsa44-ed25519@openssh.com", #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + "ssh-mldsa65-ed25519", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448", + #endif }; int typeOk = 0; int i; diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 48cdfcae0..e22f2e10c 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -1383,9 +1383,24 @@ static int test_CheckAuthKeysLineTypes(void) #endif #endif #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + "ssh-mldsa44-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) + "ssh-mldsa65-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSH_NO_ECDSA) + "ssh-mldsa87-es384", + #endif #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) "ssh-mldsa44-ed25519@openssh.com", #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + "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"; diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index d28f398b9..b1f4bd84b 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1860,6 +1860,71 @@ static int load_key_mldsa44_ed25519(byte* buf, word32* bufSz) } #endif /* !WOLFSSH_NO_MLDSA44 && !WOLFSSH_NO_ED25519 */ +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) +static int load_key_mldsa44_es256(byte* buf, word32* bufSz) +{ + word32 sz = 0; +#ifndef NO_FILESYSTEM + sz = load_file("./keys/server-key-mldsa44es256", buf, bufSz); +#else + (void)buf; (void)bufSz; +#endif + return sz; +} +#endif /* !WOLFSSH_NO_MLDSA44 && !WOLFSSH_NO_ECDSA */ + +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) +static int load_key_mldsa65_ed25519(byte* buf, word32* bufSz) +{ + word32 sz = 0; +#ifndef NO_FILESYSTEM + sz = load_file("./keys/server-key-mldsa65ed25519", buf, bufSz); +#else + (void)buf; (void)bufSz; +#endif + return sz; +} +#endif /* !WOLFSSH_NO_MLDSA65 && !WOLFSSH_NO_ED25519 */ + +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) +static int load_key_mldsa65_es256(byte* buf, word32* bufSz) +{ + word32 sz = 0; +#ifndef NO_FILESYSTEM + sz = load_file("./keys/server-key-mldsa65es256", buf, bufSz); +#else + (void)buf; (void)bufSz; +#endif + return sz; +} +#endif /* !WOLFSSH_NO_MLDSA65 && !WOLFSSH_NO_ECDSA */ + +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) +static int load_key_mldsa87_ed448(byte* buf, word32* bufSz) +{ + word32 sz = 0; +#ifndef NO_FILESYSTEM + sz = load_file("./keys/server-key-mldsa87ed448", buf, bufSz); +#else + (void)buf; (void)bufSz; +#endif + return sz; +} +#endif /* !WOLFSSH_NO_MLDSA87 && HAVE_ED448 */ + +#if !defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSH_NO_ECDSA) +static int load_key_mldsa87_es384(byte* buf, word32* bufSz) +{ + word32 sz = 0; +#ifndef NO_FILESYSTEM + sz = load_file("./keys/server-key-mldsa87es384", buf, bufSz); +#else + (void)buf; (void)bufSz; +#endif + return sz; +} +#endif /* !WOLFSSH_NO_MLDSA87 && !WOLFSSH_NO_ECDSA */ + #ifndef WOLFSSH_NO_MLDSA /* Composite keys are OpenSSH-armored envelopes holding two keys, so their * buffer must be sized from the actual file rather than a fixed constant @@ -1909,6 +1974,21 @@ typedef struct { static const MlDsaCompositeEntry mldsaCompositeEntries[] = { #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) { "mldsa44-ed25519", load_key_mldsa44_ed25519, "ML-DSA-44+Ed25519" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + { "mldsa44-es256", load_key_mldsa44_es256, "ML-DSA-44+ES256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + { "mldsa65-ed25519", load_key_mldsa65_ed25519, "ML-DSA-65+Ed25519" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) + { "mldsa65-es256", load_key_mldsa65_es256, "ML-DSA-65+ES256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + { "mldsa87-ed448", load_key_mldsa87_ed448, "ML-DSA-87+Ed448" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSH_NO_ECDSA) + { "mldsa87-es384", load_key_mldsa87_es384, "ML-DSA-87+ES384" }, #endif { NULL, NULL, NULL } }; diff --git a/keys/include.am b/keys/include.am index c86e289c4..6dea15554 100644 --- a/keys/include.am +++ b/keys/include.am @@ -26,5 +26,8 @@ EXTRA_DIST+= \ 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-mldsa44ed25519 + 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 diff --git a/keys/server-key-mldsa44es256 b/keys/server-key-mldsa44es256 new file mode 100644 index 0000000000000000000000000000000000000000..c2af21c74e981709bc9f87da9fae17deff77c3de GIT binary patch literal 2937 zcmeI!`#;kQ7zc31C?ug=q9o>$iPP7u?I6uuHYWF2Wou(D4Vzm;WI`yC-0!kn(ioMy zG|MHmTv~(^=0uTODk+Z6PvX~ zpG^Eu{+{vk@qxp)bKmdNOMV0@4gjD;>K}Ug+Y1~vYi}#4 zyoK^Adz4pZ!mp&B@RSvNie^Vuy!!Amb1Q>!pL+&A!J48i5ECqaWIw5nJ_&R!iP~p_ z>>qX{Kv|gf0bys+TUw7+9DCaEdieoM>{G6h)wiY(DQM2e*n;v>nRfzo1!Xl}1m{+V zjMW!XM4a`~+F3ObZmM#tCA*M@(14Ze)}$9v`g-Bn!?>V zP@I`<#c@eZyDl8IqF!maa&8__ay(f_xmIA>fJRQnt!zn0T^u&JdZSSSk;mumqDR;* zO}v{~7if{(t(1*)}LEgenu>f)cs z#9cac+Kf`U_k8CVu%zfhiu3Cl>X==6-+7;~vKp2Qu}zS!ENe^odN}Dw6j)irQ~FML z_oHS6JtqVZ1Aoi0hHP3{aS-Q7*%J@SYhM-AEYjLH+&qS(z{1VLQ>WsqACdVcjM<6+ ziU2a4=SG1q%UEpCI^+ni!=TyQ#%(aU(prBV#VO?3SyiDZX`u(6AqS)cRyB zIVP)$bCd1v&M5wzY3aVy^zS!Kke?8s@Ws_ywH56mq~OXEq*`*s)*KrH?0cUuop|Fg|0AcEmE9SX_=T6P6ARga#9@!df^gx;(huRJgZ5;m{ol< z7O6$kr!d6BpBtK(XfCPX+y$>0$-^<5buwaISkF>(qJuZFKZDk2-c?c}N{>#8rQW1B z3%E?kHupORjZ~9z)n$RvFlm>r-d}HFZpPI_p0+lJY*preTV)^b!=yImt=M!tOcxyi z{}R`9>12#KEh4rRqk;w{kEtDyUjs1S4mvW5iJcIJYc9YNz<=A*CqBJ9v>5Z`1<`Yy zWf6{>Y)6Q4Fj-w zqPKx9O`d#lE_rwRx?MLMjcTH28yu;W%&*0L0-VzCaq9b%iz?-@;xkdT?l&r6rdw|Y z0^+U9J;Va~7E$4<@;Wf3;;B}x>lYUC8OD8q%oSwkQC9dVKCuZh{NvLn`q1unb*G}U z817$1cX)G3632dBnu3mS+%)Ur3q$uD%lc|*zt-Ucd1TOnWhS;bdHvVsw)55MDus05_< zAL{0DBXgzBA#@CWJ+$a&T=vlXkR(}V?4spBs!;V240n_8LdCrGK?hR*{AE%Taj>0<%|Njc}nf|>$%`QK)wqx%GFT`TXAz{-uu*hJU|BX3U#7I6rx?os1 qQoB&)sbK+B@6BhL68*>SW9k||78P2`MLFIZwW0q$(;xvsu+TrjV|UvC literal 0 HcmV?d00001 diff --git a/keys/server-key-mldsa65ed25519 b/keys/server-key-mldsa65ed25519 new file mode 100644 index 0000000000000000000000000000000000000000..f53e5615e41371afda07e4299bd7654e96ac0496 GIT binary patch literal 4154 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 literal 0 HcmV?d00001 diff --git a/src/internal.c b/src/internal.c index 67ffff6b2..f0627c044 100644 --- a/src/internal.c +++ b/src/internal.c @@ -506,6 +506,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)"; @@ -521,7 +524,6 @@ const char* GetErrorString(int err) #endif } - static int wsHighwater(byte dir, void* ctx) { int ret = WS_SUCCESS; @@ -1003,9 +1005,24 @@ 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) + "ssh-mldsa87-es384," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + "ssh-mldsa65-ed25519," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) + "ssh-mldsa65-es256," +#endif #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) "ssh-mldsa44-ed25519@openssh.com," #endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + "ssh-mldsa44-es256," +#endif #ifndef WOLFSSH_NO_MLDSA87 "ssh-mldsa-87," #endif @@ -1666,7 +1683,12 @@ 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_ED25519) { + 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; if (WS_GetCompositeParams(key->keyId, ¶ms) == WS_SUCCESS) { const CompositeTradOps* ops = WS_GetTradOps(params.tradType); @@ -2334,7 +2356,12 @@ static int GetOpenSshPublicKey(WS_KeySignature *key, break; #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 = GetOpenSshKeyPublicMlDsaComposite(keyId, &key->ks.mldsa_composite.mldsa, &key->ks.mldsa_composite.trad, @@ -2476,7 +2503,12 @@ static int GetOpenSshKey(WS_KeySignature *key, break; #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 = GetOpenSshKeyMlDsaComposite( key->keyId, &key->ks.mldsa_composite.mldsa, @@ -3530,9 +3562,24 @@ 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) + { ID_MLDSA44_ES256, TYPE_KEY, "ssh-mldsa44-es256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) + { ID_MLDSA65_ES256, TYPE_KEY, "ssh-mldsa65-es256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSH_NO_ECDSA) + { ID_MLDSA87_ES384, TYPE_KEY, "ssh-mldsa87-es384" }, +#endif #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) { ID_MLDSA44_ED25519, TYPE_KEY, "ssh-mldsa44-ed25519@openssh.com" }, #endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + { 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 @@ -6426,7 +6473,12 @@ static int ParsePubKey(WOLFSSH *ssh, #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: sigKeyBlock_ptr->useMlDsaComposite = 1; sigKeyBlock_ptr->pubKeyId = ssh->handshake->pubKeyId; ret = ParseMlDsaCompositePubKey(ssh, sigKeyBlock_ptr, pubKey, pubKeySz, ssh->handshake->pubKeyId); @@ -9646,7 +9698,12 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData, } #endif #ifndef WOLFSSH_NO_MLDSA - else if (pkTypeId == ID_MLDSA44_ED25519) { + 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); } @@ -13488,7 +13545,12 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, } #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); @@ -14722,7 +14784,12 @@ static int SignH(WOLFSSH* ssh, byte* sig, word32* sigSz, break; #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 = SignHMlDsaComposite(ssh, sig, sigSz, sigKey); break; #endif @@ -15057,7 +15124,12 @@ int SendKexDhReply(WOLFSSH* ssh) ) { wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa.key); } - else if (sigKeyBlock_ptr->pubKeyId == ID_MLDSA44_ED25519) { + else if (sigKeyBlock_ptr->pubKeyId == ID_MLDSA44_ES256 + || sigKeyBlock_ptr->pubKeyId == ID_MLDSA65_ES256 + || sigKeyBlock_ptr->pubKeyId == ID_MLDSA87_ES384 + || sigKeyBlock_ptr->pubKeyId == ID_MLDSA44_ED25519 + || sigKeyBlock_ptr->pubKeyId == ID_MLDSA65_ED25519 + || sigKeyBlock_ptr->pubKeyId == ID_MLDSA87_ED448) { CompositeParams params; if (WS_GetCompositeParams(sigKeyBlock_ptr->pubKeyId, ¶ms) == WS_SUCCESS) { @@ -15208,7 +15280,12 @@ int SendKexDhReply(WOLFSSH* ssh) } 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: { /* Copy the composite key block (ML-DSA public key followed by * the traditional public key) into the buffer. */ @@ -17911,7 +17988,12 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, #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; @@ -18078,7 +18160,12 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, #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, @@ -20359,6 +20446,57 @@ int WS_GetCompositeParams(byte keyId, CompositeParams* params) params->keyId = keyId; switch (keyId) { +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + 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) + 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) + 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) case ID_MLDSA44_ED25519: params->mldsaLevel = WC_ML_DSA_44; @@ -20373,6 +20511,38 @@ int WS_GetCompositeParams(byte keyId, CompositeParams* params) params->tradSigSz = ED25519_SIG_SIZE; params->tradPrivSz = ED25519_KEY_SIZE; break; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + 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 output truncated to a fixed 64-byte digest for this + * composite's domain-separated hash; not related to SHA-512. */ + 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; @@ -20631,6 +20801,93 @@ static const CompositeTradOps compositeEd25519Ops = { }; #endif /* !WOLFSSH_NO_ED25519 */ +#ifdef HAVE_ED448 +static int CompositeEd448Init(void* key, void* heap) +{ + return wc_ed448_init_ex((ed448_key*)key, heap, INVALID_DEVID); +} + +static void CompositeEd448Free(void* key) +{ + wc_ed448_free((ed448_key*)key); +} + +static int CompositeEd448ImportPub(void* key, const byte* pub, word32 pubSz) +{ + return wc_ed448_import_public(pub, pubSz, (ed448_key*)key); +} + +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); +} + +static int CompositeEd448ExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ed448_export_public((ed448_key*)key, out, outSz); +} + +static int CompositeEd448Sign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* m_prime, word32 m_primeLen, + 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(m_prime, m_primeLen, wireSig, &sigSz, + (ed448_key*)key, NULL, 0); + if (ret != 0 || sigSz != ED448_SIG_SIZE) { + ret = WS_ED448_E; + } + else { + *wireSigSz = sigSz; + } + + return ret; +} + +static int CompositeEd448Verify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* m_prime, word32 m_primeLen) +{ + int ret; + int res = 0; + + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + ret = wc_ed448_verify_msg(wireSig, wireSigSz, m_prime, m_primeLen, + &res, (ed448_key*)key, NULL, 0); + if (ret != 0 || res != 1) { + ret = WS_ED448_E; + } + + return ret; +} + +static const CompositeTradOps compositeEd448Ops = { + TRAD_TYPE_ED448, + CompositeEd448Init, CompositeEd448Free, + CompositeEd448ImportPub, CompositeEd448ImportPriv, + CompositeEd448ExportPub, + CompositeEd448Sign, CompositeEd448Verify +}; +#endif /* HAVE_ED448 */ + const CompositeTradOps* WS_GetTradOps(byte tradType) { switch (tradType) { @@ -20641,6 +20898,10 @@ const CompositeTradOps* WS_GetTradOps(byte tradType) #ifndef WOLFSSH_NO_ED25519 case TRAD_TYPE_ED25519: return &compositeEd25519Ops; +#endif +#ifdef HAVE_ED448 + case TRAD_TYPE_ED448: + return &compositeEd448Ops; #endif default: return NULL; diff --git a/src/keygen.c b/src/keygen.c index 719a19107..ac60e44ac 100644 --- a/src/keygen.c +++ b/src/keygen.c @@ -400,6 +400,21 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, 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; diff --git a/tests/kex.c b/tests/kex.c index 1059f15a8..e8bcc65a5 100644 --- a/tests/kex.c +++ b/tests/kex.c @@ -522,6 +522,26 @@ int wolfSSH_KexTest(int argc, char** argv) wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa44-ed25519@openssh.com"), EXIT_SUCCESS); #endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa44-es256"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa65-es256"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa65-ed25519"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSH_NO_ECDSA) + 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 e1955b8ff..0c5f2e322 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -885,6 +885,21 @@ static int test_MlDsaCompositeKeyGen(void) #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) { WOLFSSH_MLDSAKEY_44, WOLFSSH_COMPOSITE_TRAD_ED25519, "44+Ed25519" }, #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + { WOLFSSH_MLDSAKEY_44, WOLFSSH_COMPOSITE_TRAD_ECDSA, "44+ES256" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + { WOLFSSH_MLDSAKEY_65, WOLFSSH_COMPOSITE_TRAD_ED25519, "65+Ed25519" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) + { 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) + { WOLFSSH_MLDSAKEY_87, WOLFSSH_COMPOSITE_TRAD_ECDSA, "87+ES384" }, + #endif }; const word32 bufSz = 8192; word32 i; @@ -4662,7 +4677,7 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, } else if (params.tradType == TRAD_TYPE_ECC) { #ifndef WOLFSSH_NO_ECDSA - int keysz = (int)params.tradPrivSz; + 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; } @@ -4959,9 +4974,29 @@ static int test_SignHMlDsaComposite_Params(const char* label, byte keyId) static int test_SignHMlDsaComposite(void) { int ret = 0; +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + 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) + 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) + 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) 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) + 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; } @@ -5381,11 +5416,41 @@ static int test_BuildUserAuthRequestMlDsaComposite_Params( static int test_BuildUserAuthRequestMlDsaComposite(void) { int ret = 0; +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + 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) + 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) + 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) 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) + 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; } @@ -5394,11 +5459,41 @@ static int test_BuildUserAuthRequestMlDsaComposite(void) static int test_DoUserAuthRequestMlDsaComposite(void) { int ret = 0; +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA) + 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; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ECDSA) + 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; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSH_NO_ECDSA) + 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; +#endif #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) 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; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) + 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; +#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; #endif return 0; } diff --git a/wolfssh/error.h b/wolfssh/error.h index ff3a4b607..a234ca4ad 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 this to indicate last error */ }; diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 4058732b1..78c859a56 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -411,7 +411,12 @@ enum { /* Always declared, even when a sub-algorithm or MLDSA level is * unavailable; NameIdMap and WS_GetCompositeParams() separately gate * which of these are actually reachable at runtime. */ + 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,