From c4815deaa031cbcbc1eeb446bb1e8bd49690c532 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 | 118 +- apps/wolfsshd/auth.h | 4 + apps/wolfsshd/test/test_configuration.c | 264 +++- apps/wolfsshd/wolfsshd.c | 67 +- examples/echoserver/echoserver.c | 110 +- keys/include.am | 2 +- keys/server-key-mldsa44ed25519 | Bin 0 -> 2894 bytes src/internal.c | 1476 ++++++++++++++++++++++- src/keygen.c | 328 +++++ src/ossh.c | 160 ++- tests/kex.c | 7 + tests/unit.c | 1004 +++++++++++++++ wolfssh/internal.h | 110 ++ wolfssh/keygen.h | 11 + 14 files changed, 3600 insertions(+), 61 deletions(-) create mode 100644 keys/server-key-mldsa44ed25519 diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 4ed6bd81a..43918ffdd 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -53,6 +53,7 @@ #include #include #include +#include #ifdef WOLFSSL_FPKI #include @@ -147,14 +148,41 @@ struct WOLFSSHD_AUTH { #endif #ifndef MAX_LINE_SZ - /* Sized to hold the largest authorized_keys entry. */ + /* sized for the largest authorized_keys entry, composite pubkeys + * included */ #ifndef WOLFSSH_NO_MLDSA #ifndef WOLFSSH_NO_MLDSA87 - #define MAX_LINE_SZ ((WC_MLDSA_87_PUB_KEY_SIZE + 2) / 3 * 4 + 640) + #if defined(WOLFSSH_CERTS) + /* x509v3-ssh-mldsa-87: ML-DSA-87 pubkey + CA sig + DER + * overhead, 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 @@ -257,9 +285,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; @@ -276,8 +305,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; } @@ -996,6 +1026,78 @@ static int SearchKeysFile(const char* keysFilePath, const byte* key, return ret; } +/* Detects OpenSSH vs ASN1/DER format of a raw host private key buffer. + * + * Uses wc_KeyPemToDer(), not wc_PemToDer(..., PRIVATEKEY_TYPE, ...): the + * latter also unwraps PKCS#8 via ToTraditional(), which mangles key types + * with no traditional DER form (e.g. ML-DSA). + * + * On a PEM buffer, *keyDer is a WMALLOC'd (heap, DYNTYPE_SSHD) buffer the + * caller must WS_FORCEZERO + WFREE; NULL if data was passed through as-is + * (raw DER or OpenSSH). privBuf/privBufSz are set to the buffer to actually + * load. Returns WOLFSSH_FORMAT_ASN1/WOLFSSH_FORMAT_OPENSSH, or negative on + * error. */ +int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, void* heap, + byte** keyDer, byte** privBuf, word32* privBufSz) +{ + int keyFormat = WOLFSSH_FORMAT_ASN1; + byte* der; + int derSz; + + if (keyDer != NULL) { + *keyDer = NULL; + } + if (privBuf != NULL) { + *privBuf = NULL; + } + if (privBufSz != NULL) { + *privBufSz = 0; + } + + if (data == NULL || dataSz == 0 || keyDer == NULL || privBuf == NULL || + privBufSz == NULL) { + return WS_BAD_ARGUMENT; + } + + der = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD); + if (der == NULL) { + return WS_MEMORY_E; + } + + derSz = wc_KeyPemToDer(data, (int)dataSz, der, (int)dataSz, NULL); + if (derSz <= 0) { + WFREE(der, heap, DYNTYPE_SSHD); + + *privBuf = data; + *privBufSz = dataSz; + + /* wstrnstr() stops at the first NUL, so binary buffers fall + * through to the WMEMCMP magic check below. */ + if (WSTRNSTR((const char*)*privBuf, + "-----BEGIN OPENSSH PRIVATE KEY-----", *privBufSz) != NULL) { + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + else if (*privBufSz >= sizeof("openssh-key-v1") && + WMEMCMP(*privBuf, "openssh-key-v1", + sizeof("openssh-key-v1")) == 0) { + /* sizeof() includes the magic's trailing NUL */ + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + } + else { + *keyDer = der; + *privBuf = der; + *privBufSz = (word32)derSz; + /* PEM-decoded result may still be an OpenSSH binary blob */ + if (*privBufSz >= sizeof("openssh-key-v1") && + WMEMCMP(*privBuf, "openssh-key-v1", + sizeof("openssh-key-v1")) == 0) { + keyFormat = WOLFSSH_FORMAT_OPENSSH; + } + } + + return keyFormat; +} WOLFSSHD_STATIC int SearchForPubKey(const char* path, const char* authKeysFile, const char* user, diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 78bc2c905..848401e6b 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -101,6 +101,10 @@ int wolfSSHD_GetHomeDirectory(WOLFSSHD_AUTH* auth, WOLFSSH* ssh, WCHAR* out, int int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid, int rejectReadable, void* heap, WFILE** out); +/* classifies a loaded host private key buffer as OpenSSH or ASN1/DER */ +int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, void* heap, + byte** keyDer, byte** privBuf, word32* privBufSz); + #ifdef WOLFSSHD_UNIT_TEST #ifndef _WIN32 extern int (*wsshd_setregid_cb)(WGID_T, WGID_T); diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 4f9d6c44a..06c250efd 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -23,6 +23,7 @@ #endif #include +#include #include #include #include @@ -1406,18 +1407,19 @@ static int test_CheckPasswordHashUnix(void) #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ #ifdef WOLFSSL_BASE64_ENCODE -/* Build a mutable "ssh-rsa " line; WSTRTOK mutates in place. */ -static int BuildAuthKeysLine(const byte* key, word32 keySz, - char* lineOut, word32 lineOutSz) +/* Build a mutable " " line; WSTRTOK mutates in place. */ +static int BuildAuthKeysLineType(const char* type, const byte* key, + word32 keySz, char* lineOut, word32 lineOutSz) { - static const char prefix[] = "ssh-rsa "; - word32 prefixLen = (word32)(sizeof(prefix) - 1); + word32 typeLen = (word32)WSTRLEN(type); + word32 prefixLen = typeLen + 1; word32 b64Sz; if (lineOutSz <= prefixLen) { return WS_BUFFER_E; } - WMEMCPY(lineOut, prefix, prefixLen); + WMEMCPY(lineOut, type, typeLen); + lineOut[typeLen] = ' '; b64Sz = lineOutSz - prefixLen; if (Base64_Encode_NoNl(key, keySz, (byte*)lineOut + prefixLen, &b64Sz) != 0) { @@ -1431,7 +1433,111 @@ static int BuildAuthKeysLine(const byte* key, word32 keySz, return WS_SUCCESS; } -/* Negative-path coverage for CheckAuthKeysLine's ConstantCompare clause. */ +static int BuildAuthKeysLine(const byte* key, word32 keySz, + char* lineOut, word32 lineOutSz) +{ + return BuildAuthKeysLineType("ssh-rsa", key, keySz, lineOut, lineOutSz); +} + +/* Confirms every key-type string in CheckAuthKeysLine's allowedTypes[] table + * is recognized, guarding against 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. */ static int test_CheckAuthKeysLine(void) { int ret = WS_SUCCESS; @@ -3722,6 +3828,148 @@ static int test_ResolveAuthKeysPath(void) return ret; } +/* read an entire file into a heap buffer; *outSz is set to the file size. + * returns NULL on any failure */ +static byte* ReadWholeFile(const char* path, word32* outSz) +{ + FILE* f; + byte* buf = NULL; + long sz; + + f = fopen(path, "rb"); + if (f == NULL) { + return NULL; + } + if (fseek(f, 0, SEEK_END) != 0 || (sz = ftell(f)) < 0 || + fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + buf = (byte*)malloc((size_t)sz); + if (buf != NULL) { + if (fread(buf, 1, (size_t)sz, f) != (size_t)sz) { + free(buf); + buf = NULL; + } + } + fclose(f); + if (buf != NULL) { + *outSz = (word32)sz; + } + return buf; +} + +/* locate the repo's keys/ directory regardless of whether this binary is run + * from the repo root or from apps/wolfsshd/test/ */ +static int BuildKeyPath(const char* name, char* out, size_t outSz) +{ + static const char* candidates[] = { "keys/", "../../../keys/" }; + word32 i; + FILE* f; + + for (i = 0; i < (word32)(sizeof(candidates) / sizeof(candidates[0])); + i++) { + snprintf(out, outSz, "%s%s", candidates[i], name); + f = fopen(out, "rb"); + if (f != NULL) { + fclose(f); + return WS_SUCCESS; + } + } + return WS_FATAL_ERROR; +} + +/* Regression coverage for wolfSSHD_DetectPrivKeyFormat(), the host-key + * format auto-detection SetupCTX() relies on to load PEM-armored OpenSSH + * keys, raw binary openssh-key-v1 blobs (including composite ML-DSA host + * keys, which are only ever stored in that raw form), 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; + byte* badKeyDer = NULL; + byte* badPrivBuf = NULL; + word32 badPrivBufSz = 0; + int badGot; + + /* A 0-byte host key file (or otherwise bad arguments) must be rejected + * without touching the out-params, matching the empty-file case + * getBufferFromFile() can hand back. */ + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, 0, NULL, &badKeyDer, + &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), NULL, + &badKeyDer, &badPrivBuf, &badPrivBufSz); + Log(" Testing scenario: NULL data pointer. %s\n", + (badGot == WS_BAD_ARGUMENT) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT) { + return WS_FATAL_ERROR; + } + + for (i = 0; i < (word32)(sizeof(cases) / sizeof(cases[0])); i++) { + char path[128]; + byte* data; + word32 dataSz = 0; + byte* keyDer = NULL; + byte* privBuf = NULL; + word32 privBufSz = 0; + int gotFormat; + + if (BuildKeyPath(cases[i].file, path, sizeof(path)) != WS_SUCCESS) { + Log(" Testing scenario: %s. FAILED (couldn't locate %s)\n", + cases[i].desc, cases[i].file); + return WS_FATAL_ERROR; + } + + data = ReadWholeFile(path, &dataSz); + if (data == NULL) { + Log(" Testing scenario: %s. FAILED (couldn't read %s)\n", + cases[i].desc, path); + return WS_FATAL_ERROR; + } + + gotFormat = wolfSSHD_DetectPrivKeyFormat(data, dataSz, NULL, &keyDer, + &privBuf, &privBufSz); + + Log(" Testing scenario: %s. %s\n", cases[i].desc, + (gotFormat == cases[i].wantFormat) ? "PASSED" : "FAILED"); + if (gotFormat != cases[i].wantFormat) { + ret = WS_FATAL_ERROR; + } + + if (keyDer != NULL) { + WFREE(keyDer, NULL, DYNTYPE_SSHD); + } + free(data); + if (ret != WS_SUCCESS) { + return ret; + } + } + + return ret; +} + const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_ParseConfigLine), @@ -3747,8 +3995,10 @@ const TEST_CASE testCases[] = { TEST_DECL(test_OpenSecureFile), TEST_DECL(test_ConfigSavePID), #endif + TEST_DECL(test_DetectPrivKeyFormat), #ifdef WOLFSSL_BASE64_ENCODE TEST_DECL(test_CheckAuthKeysLine), + TEST_DECL(test_CheckAuthKeysLineTypes), #endif #if defined(WOLFSSL_BASE64_ENCODE) && !defined(_WIN32) TEST_DECL(test_SearchForPubKey), diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 54031b6f8..e30148272 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -349,6 +349,7 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, byte** banner) { int ret = WS_SUCCESS; + byte* keyDer = NULL; byte* privBuf = NULL; word32 privBufSz = 0; void* heap = NULL; @@ -411,20 +412,13 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } if (ret == WS_SUCCESS) { - /* Host keys may be PEM or DER. Detect by content: a DER key is - * an ASN.1 SEQUENCE (leading 0x30); anything else is treated as - * PEM text and decoded with wc_KeyPemToDer(), which handles - * PKCS#1, SEC1 and PKCS#8 "PRIVATE KEY" bodies. - * - * The previous code used wc_PemToDer(..., PRIVATEKEY_TYPE, ...), - * which only recognizes the classic "RSA/EC PRIVATE KEY" PEM - * headers. On a PKCS#8 body (how ML-DSA host keys are emitted) - * it returns *success* but yields a malformed body (leading - * 0x04, not a 0x30 SEQUENCE), which - * wolfSSH_CTX_UsePrivateKey_buffer() then rejects with - * WS_BAD_FILETYPE_E, so ML-DSA PEM host keys could not load. */ - byte* keyDer = NULL; - + /* Host keys may be OpenSSH, PEM, or DER. + * wolfSSHD_DetectPrivKeyFormat() uses wc_KeyPemToDer() (not + * wc_PemToDer(..., PRIVATEKEY_TYPE, ...), which unwraps + * PKCS#8 via ToTraditional() and mangles key types with no + * traditional DER form, e.g. ML-DSA) and also detects the + * OpenSSH private-key format, used by composite ML-DSA host + * keys generated in that format. */ if (dataSz == 0) { /* An empty (0-byte) file passes the NULL check above but * carries no key material. Handle it explicitly as a file @@ -434,36 +428,31 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Host key file is empty."); ret = WS_BAD_FILE_E; } - else if (data[0] == 0x30) { - privBuf = data; - privBufSz = dataSz; - } else { - keyDer = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD); - if (keyDer == NULL) { - ret = WS_MEMORY_E; + int keyFormat = wolfSSHD_DetectPrivKeyFormat(data, dataSz, + heap, &keyDer, &privBuf, &privBufSz); + + if (keyFormat < 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Host private key file is invalid."); + ret = WS_BAD_FILE_E; + } + else if (keyFormat == WOLFSSH_FORMAT_OPENSSH) { + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " + "key as OpenSSH format."); } else { - int keyDerSz = wc_KeyPemToDer(data, dataSz, keyDer, - (int)dataSz, NULL); - if (keyDerSz <= 0) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to convert " - "host private key from PEM."); - ret = WS_BAD_FILE_E; - } - else { - privBuf = keyDer; - privBufSz = (word32)keyDerSz; - } + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " + "key as DER format."); } - } - if (ret == WS_SUCCESS - && wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, - privBufSz, WOLFSSH_FORMAT_ASN1) < 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to use host private key."); - ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && + wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, + privBufSz, keyFormat) < 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to use host private key."); + ret = WS_BAD_ARGUMENT; + } } if (keyDer != NULL) { diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 92ea43bbf..c1a485371 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1851,8 +1851,70 @@ static int load_key_mldsa87(byte* buf, word32 bufSz) } #endif /* WOLFSSH_NO_MLDSA87 */ +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +/* buf NULL: *bufSz set to file size; else file read into buf (bufSz max) */ +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 key buffer must be sized from the file, not a fixed constant */ +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 is never empty if ECDSA and Ed25519/Ed448 + * are both disabled */ +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; @@ -3363,13 +3425,59 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) * unconditionally would force mldsa negotiation on non-mldsa tests. */ #ifndef WOLFSSH_NO_MLDSA if (keyList != NULL && WSTRSTR(keyList, "mldsa") != NULL) { - if (LoadMlDsaHostKeys(ctx, keyList) != 0) { + int mldsaErr = 0; + int mldsaMatched = 0; + + /* skip LoadMlDsaHostKeys() for a purely composite keyList; it + * only knows plain "mldsa-NN" names and would abort */ + if (WSTRSTR(keyList, "mldsa-44") != NULL || + WSTRSTR(keyList, "mldsa-65") != NULL || + WSTRSTR(keyList, "mldsa-87") != NULL) { + mldsaMatched = 1; + if (LoadMlDsaHostKeys(ctx, keyList) != 0) { + mldsaErr = 1; + } + } + + if (mldsaErr) { #ifdef WOLFSSH_SMALL_STACK wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); WFREE(keyLoadBuf, NULL, 0); #endif ES_ERROR("Error loading ML-DSA host keys.\n"); } + else { + word32 mldsaIdx; + + for (mldsaIdx = 0; + mldsaCompositeEntries[mldsaIdx].substr != NULL; + mldsaIdx++) { + const MlDsaCompositeEntry* entry = + &mldsaCompositeEntries[mldsaIdx]; + + if (WSTRSTR(keyList, entry->substr) != NULL) { + mldsaMatched = 1; + if (LoadMlDsaCompositeHostKey(ctx, entry->loadFn, + entry->label) != 0) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif + ES_ERROR("Error loading %s host key.\n", + entry->label); + } + } + } + + if (!mldsaMatched) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif + ES_ERROR("ML-DSA key list '%s' matched no supported " + "level.\n", keyList); + } + } } #endif /* WOLFSSH_NO_MLDSA */ diff --git a/keys/include.am b/keys/include.am index 99dcb4ac0..82094705c 100644 --- a/keys/include.am +++ b/keys/include.am @@ -26,7 +26,7 @@ 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 \ keys/renew-ossh-certs.sh \ keys/ossh-ca keys/ossh-ca.pub \ keys/ossh-ca-rsa keys/ossh-ca-rsa.pub \ diff --git a/keys/server-key-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 || @@ -2483,6 +2496,9 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, heap = ctx->heap; + if (format == WOLFSSH_FORMAT_OPENSSH && type != BUFTYPE_PRIVKEY) + return WS_UNIMPLEMENTED_E; + if (format == WOLFSSH_FORMAT_ASN1 || format == WOLFSSH_FORMAT_RAW) { if (in[0] != 0x30) return WS_BAD_FILETYPE_E; @@ -2492,6 +2508,52 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, WMEMCPY(der, in, inSz); derSz = inSz; } + else if (format == WOLFSSH_FORMAT_OPENSSH) { + der = (byte*)WMALLOC(inSz, heap, dynamicType); + if (der == NULL) + return WS_MEMORY_E; + /* strip PEM wrapper so IdentifyOpenSshKey sees raw binary; mirrors + * DoOpenSshKey() in src/ssh.c since wc_KeyPemToDer() doesn't know + * the OPENSSH PRIVATE KEY header */ + 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) { + WS_FORCEZERO(der, inSz); + WFREE(der, heap, dynamicType); + return WS_BAD_FILE_E; + } + derSz = derOutSz; + } + else { + WMEMCPY(der, in, inSz); + derSz = inSz; + } + } else if (format == WOLFSSH_FORMAT_PEM) { /* The der size will be smaller than the pem size. */ der = (byte*)WMALLOC(inSz, heap, dynamicType); @@ -2533,7 +2595,10 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, /* Maybe decrypt */ if (type == BUFTYPE_PRIVKEY) { - ret = IdentifyAsn1Key(der, derSz, 1, ctx->heap, NULL); + if (format == WOLFSSH_FORMAT_OPENSSH) + ret = IdentifyOpenSshKey(der, derSz, ctx->heap); + else + ret = IdentifyAsn1Key(der, derSz, 1, ctx->heap, NULL); if (ret < 0) { if (der != NULL) { WS_FORCEZERO(der, derSz); @@ -2542,6 +2607,16 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, return ret; } keyId = (byte)ret; + /* Only the composite key parsers know how to walk the stored + * openssh-key-v1 envelope later (SendKexGetSigningKey()); every + * other key type expects raw DER, so reject them here instead of + * silently storing an envelope that will fail every handshake. */ + if (format == WOLFSSH_FORMAT_OPENSSH && + keyId != ID_MLDSA44_ED25519) { + WS_FORCEZERO(der, derSz); + WFREE(der, heap, dynamicType); + return WS_UNIMPLEMENTED_E; + } ret = SetHostPrivateKey(ctx, keyId, der, derSz, dynamicType); } #ifdef WOLFSSH_CERTS @@ -2917,6 +2992,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 @@ -4162,6 +4240,9 @@ static const byte cannedKeyAlgoClient[] = { #endif /* WOLFSSH_NO_SSH_RSA_SHA1 */ #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ #endif /* WOLFSSH_CERTS */ +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + ID_MLDSA44_ED25519, +#endif #ifndef WOLFSSH_NO_MLDSA87 ID_MLDSA87, #endif @@ -5356,7 +5437,9 @@ struct wolfSSH_sigKeyBlock { byte useEcc:1; byte useMlDsa:1; byte useEd25519:1; + byte useMlDsaComposite:1; byte keyAllocated:1; + byte pubKeyId; word32 keySz; union { #ifndef WOLFSSH_NO_RSA @@ -5378,6 +5461,32 @@ 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) + /* keep union non-empty (empty union rejected by some + * compilers) though unusable without a trad component */ + byte placeholder; +#endif + } trad; + /* largest mldsaPubSz + tradPubSz across WS_GetCompositeParams() + * combos; keep in sync with any new combo added there */ + byte q[WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; + word32 qSz; + } mldsa_composite; #endif } sk; }; @@ -5570,6 +5679,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, @@ -5959,6 +6092,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; @@ -6025,6 +6165,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; } } @@ -6890,6 +7042,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; } @@ -8370,7 +8536,6 @@ static int DoUserAuthRequestRsaCert(WOLFSSH* ssh, WS_UserAuthData_PublicKey* pk, #ifndef WOLFSSH_NO_ECDSA -#define ECDSA_ASN_SIG_SZ 256 /* Utility for DoUserAuthRequestPublicKey() */ /* returns negative for error, positive is size of digest. */ @@ -9449,6 +9614,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; @@ -12775,6 +12946,32 @@ 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) + /* keep union non-empty (empty union rejected by some + * compilers) though unusable without a trad component */ + byte placeholder; +#endif + } trad; + /* largest mldsaPubSz + tradPubSz across + * WS_GetCompositeParams() combos; keep in sync */ + byte q[WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; + word32 qSz; + } mldsa_composite; #endif } sk; }; @@ -12782,7 +12979,8 @@ struct wolfSSH_sigKeyBlockFull { #ifdef WOLFSSH_NO_MLDSA #define KEX_SIG_SIZE (512) #else - #define KEX_SIG_SIZE MLDSA_MAX_SIG_SIZE + /* covers the largest trad signature appended by SignHMlDsaComposite() */ + #define KEX_SIG_SIZE (MLDSA_MAX_SIG_SIZE + COMPOSITE_MAX_TRAD_SIG_SZ) #endif #ifdef WOLFSSH_CERTS @@ -13425,6 +13623,103 @@ 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) { + /* shallow copy: ownership moves, don't reuse keySig */ + 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; @@ -14575,6 +14870,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; @@ -14907,6 +15207,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 } @@ -15046,6 +15357,17 @@ int SendKexDhReply(WOLFSSH* ssh) idx += sigKeyBlock_ptr->sk.mldsa.qSz; } break; + + case ID_MLDSA44_ED25519: + { + /* composite key block: ML-DSA pubkey followed by trad pubkey */ + c32toa(sigKeyBlock_ptr->sk.mldsa_composite.qSz, output + idx); + idx += LENGTH_SZ; + WMEMCPY(output + idx, sigKeyBlock_ptr->sk.mldsa_composite.q, + sigKeyBlock_ptr->sk.mldsa_composite.qSz); + idx += sigKeyBlock_ptr->sk.mldsa_composite.qSz; + } + break; #endif #ifdef WOLFSSH_CERTS @@ -17936,6 +18258,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; } @@ -18109,6 +18437,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; } @@ -20395,7 +20738,1132 @@ 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; + } + + /* guards mPrime buffer sizing in VerifyMlDsaComposite/ + * SignHMlDsaComposite; fail loudly instead of overflowing */ + if (params->labelSz > COMPOSITE_MAX_LABEL_SZ) { + WLOG(WS_LOG_ERROR, "Composite label size %u exceeds " + "COMPOSITE_MAX_LABEL_SZ %u", params->labelSz, + (word32)COMPOSITE_MAX_LABEL_SZ); + return WS_BUFFER_E; + } + + return WS_SUCCESS; +} + +int WS_Hash_Helper(enum wc_HashType hashId, const byte* msg, word32 msgSz, byte* hash, word32 hashSz) +{ + int ret; +#ifdef WOLFSSL_SHAKE256 + if (hashId == WC_HASH_TYPE_SHAKE256) { + wc_Shake shake; + ret = wc_InitShake256(&shake, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_Shake256_Update(&shake, msg, msgSz); + if (ret == 0) { + ret = wc_Shake256_Final(&shake, hash, hashSz); + } + } + return ret; + } +#endif + return wc_Hash(hashId, msg, msgSz, hash, hashSz); +} + +/* one CompositeTradOps instance per trad algorithm; see wolfssh/internal.h */ + +#ifndef WOLFSSH_NO_ECDSA +/* returns 0 on success, negative on failure (wc_ecc_init_ex() code) */ +static int CompositeEccInit(void* key, void* heap) +{ + return wc_ecc_init_ex((ecc_key*)key, heap, INVALID_DEVID); +} + +/* no return value */ +static void CompositeEccFree(void* key) +{ + wc_ecc_free((ecc_key*)key); +} + +/* returns 0 on success, negative on failure (wc_ecc_import_x963() code) */ +static int CompositeEccImportPub(void* key, const byte* pub, word32 pubSz) +{ + return wc_ecc_import_x963(pub, pubSz, (ecc_key*)key); +} + +/* returns 0 on success, negative wc_ecc_import_private_key() code on + * failure */ +static int CompositeEccImportPriv(void* key, const byte* priv, word32 privSz, + const byte* pub, word32 pubSz) +{ + return wc_ecc_import_private_key(priv, privSz, pub, pubSz, (ecc_key*)key); +} + +/* returns 0 on success, negative on failure (wc_ecc_export_x963() code) */ +static int CompositeEccExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ecc_export_x963((ecc_key*)key, out, outSz); +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEccSign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* mPrime, word32 mPrimeLen, + byte* wireSig, word32* wireSigSz) +{ + int ret; + word32 asnSigSz = ECDSA_ASN_SIG_SZ; + byte digest[WC_MAX_DIGEST_SIZE]; +#ifdef WOLFSSH_SMALL_STACK + byte* asnSig = NULL; +#else + byte asnSig[ECDSA_ASN_SIG_SZ]; +#endif + + if (tradHashSz > WC_MAX_DIGEST_SIZE) + return WS_BUFFER_E; + +#ifdef WOLFSSH_SMALL_STACK + asnSig = (byte*)WMALLOC(ECDSA_ASN_SIG_SZ, heap, DYNTYPE_TEMP); + if (asnSig == NULL) + return WS_MEMORY_E; +#else + (void)heap; +#endif + + ret = WS_Hash_Helper(tradHashId, mPrime, mPrimeLen, digest, tradHashSz); + if (ret == 0) { + ret = wc_ecc_sign_hash(digest, tradHashSz, asnSig, &asnSigSz, + rng, (ecc_key*)key); + } + if (ret == 0) { + word32 rSz = MAX_ECC_BYTES + ECC_MAX_PAD_SZ, + sSz = MAX_ECC_BYTES + ECC_MAX_PAD_SZ; +#ifdef WOLFSSH_SMALL_STACK + byte* rBuf = NULL; + byte* sBuf = NULL; + + rBuf = (byte*)WMALLOC(MAX_ECC_BYTES + ECC_MAX_PAD_SZ, heap, + DYNTYPE_TEMP); + if (rBuf == NULL) + ret = WS_MEMORY_E; + if (ret == 0) { + sBuf = (byte*)WMALLOC(MAX_ECC_BYTES + ECC_MAX_PAD_SZ, heap, + DYNTYPE_TEMP); + if (sBuf == NULL) + ret = WS_MEMORY_E; + } +#else + byte rBuf[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; + byte sBuf[MAX_ECC_BYTES + ECC_MAX_PAD_SZ]; +#endif + + if (ret == 0) { + ret = wc_ecc_sig_to_rs(asnSig, asnSigSz, rBuf, &rSz, sBuf, &sSz); + } + if (ret == 0) { + word32 offset = 0; + byte rPad = (rBuf[0] & 0x80) ? 1 : 0; + byte sPad = (sBuf[0] & 0x80) ? 1 : 0; + + /* RFC 5656 3.1.2: r/s are mpints; a positive value with its + * top bit set needs a leading zero pad byte. */ + if (*wireSigSz < (2U * LENGTH_SZ) + rSz + rPad + sSz + sPad) { + ret = WS_BAD_ARGUMENT; + } + else { + c32toa(rSz + rPad, wireSig + offset); + offset += LENGTH_SZ; + if (rPad) + wireSig[offset++] = 0; + WMEMCPY(wireSig + offset, rBuf, rSz); + offset += rSz; + + c32toa(sSz + sPad, wireSig + offset); + offset += LENGTH_SZ; + if (sPad) + wireSig[offset++] = 0; + WMEMCPY(wireSig + offset, sBuf, sSz); + offset += sSz; + + *wireSigSz = offset; + } + } +#ifdef WOLFSSH_SMALL_STACK + if (rBuf != NULL) { + WFREE(rBuf, heap, DYNTYPE_TEMP); + } + if (sBuf != NULL) { + WFREE(sBuf, heap, DYNTYPE_TEMP); + } +#endif + } + if (ret != 0 && ret != WS_BAD_ARGUMENT) { + ret = WS_ECC_E; + } + +#ifdef WOLFSSH_SMALL_STACK + if (asnSig != NULL) { + WFREE(asnSig, heap, DYNTYPE_TEMP); + } +#endif + + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEccVerify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* mPrime, word32 mPrimeLen) +{ + int ret; + const byte* r = NULL; + const byte* s = NULL; + word32 rSz = 0, sSz = 0; + word32 i = 0; + word32 asnSigSz = ECDSA_ASN_SIG_SZ; +#ifdef WOLFSSH_SMALL_STACK + byte* asnSig = NULL; +#else + byte asnSig[ECDSA_ASN_SIG_SZ]; +#endif + + if (tradHashSz > WC_MAX_DIGEST_SIZE) + return WS_BUFFER_E; + +#ifdef WOLFSSH_SMALL_STACK + asnSig = (byte*)WMALLOC(ECDSA_ASN_SIG_SZ, heap, DYNTYPE_TEMP); + if (asnSig == NULL) + return WS_MEMORY_E; +#else + (void)heap; +#endif + + ret = GetStringRef(&rSz, &r, wireSig, wireSigSz, &i); + if (ret == WS_SUCCESS) { + ret = GetStringRef(&sSz, &s, wireSig, wireSigSz, &i); + } + if (ret == WS_SUCCESS) { + ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz, asnSig, &asnSigSz); + if (ret != 0) ret = WS_ECC_E; + } + if (ret == WS_SUCCESS) { + byte digest[WC_MAX_DIGEST_SIZE]; + ret = WS_Hash_Helper(tradHashId, mPrime, mPrimeLen, digest, tradHashSz); + if (ret == 0) { + ret = wc_SignatureVerifyHash( + tradHashId, + WC_SIGNATURE_TYPE_ECC, + digest, tradHashSz, + asnSig, asnSigSz, + (ecc_key*)key, + sizeof(ecc_key)); + } + if (ret != 0) { + ret = WS_ECC_E; + } + } + +#ifdef WOLFSSH_SMALL_STACK + if (asnSig != NULL) { + WFREE(asnSig, heap, DYNTYPE_TEMP); + } +#endif + + return ret; +} + +static const CompositeTradOps compositeEccOps = { + CompositeEccInit, CompositeEccFree, + CompositeEccImportPub, CompositeEccImportPriv, CompositeEccExportPub, + CompositeEccSign, CompositeEccVerify, + TRAD_TYPE_ECC +}; +#endif /* !WOLFSSH_NO_ECDSA */ + +#ifndef WOLFSSH_NO_ED25519 +/* returns 0 on success, negative on failure (wc_ed25519_init_ex() code) */ +static int CompositeEd25519Init(void* key, void* heap) +{ + return wc_ed25519_init_ex((ed25519_key*)key, heap, INVALID_DEVID); +} + +/* no return value */ +static void CompositeEd25519Free(void* key) +{ + wc_ed25519_free((ed25519_key*)key); +} + +/* returns 0 on success, negative wc_ed25519_import_public() code on + * failure */ +static int CompositeEd25519ImportPub(void* key, const byte* pub, word32 pubSz) +{ + return wc_ed25519_import_public(pub, pubSz, (ed25519_key*)key); +} + +/* returns 0 on success, negative wc_ed25519_import_private_key() code on + * failure */ +static int CompositeEd25519ImportPriv(void* key, const byte* priv, + word32 privSz, const byte* pub, word32 pubSz) +{ + return wc_ed25519_import_private_key(priv, privSz, pub, pubSz, + (ed25519_key*)key); +} + +/* returns 0 on success, negative wc_ed25519_export_public() code on + * failure */ +static int CompositeEd25519ExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ed25519_export_public((ed25519_key*)key, out, outSz); +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEd25519Sign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* mPrime, word32 mPrimeLen, + byte* wireSig, word32* wireSigSz) +{ + int ret; + word32 sigSz = ED25519_SIG_SIZE; + + (void)rng; + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + if (*wireSigSz < ED25519_SIG_SIZE) { + return WS_BAD_ARGUMENT; + } + + ret = wc_ed25519_sign_msg(mPrime, mPrimeLen, wireSig, &sigSz, + (ed25519_key*)key); + if (ret != 0 || sigSz != ED25519_SIG_SIZE) { + ret = WS_ED25519_E; + } + else { + *wireSigSz = sigSz; + } + + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEd25519Verify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* mPrime, word32 mPrimeLen) +{ + int ret; + int res = 0; + + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + ret = wc_ed25519_verify_msg(wireSig, wireSigSz, mPrime, mPrimeLen, + &res, (ed25519_key*)key); + if (ret != 0 || res != 1) { + ret = WS_ED25519_E; + } + + return ret; +} + +static const CompositeTradOps compositeEd25519Ops = { + CompositeEd25519Init, CompositeEd25519Free, + CompositeEd25519ImportPub, CompositeEd25519ImportPriv, + CompositeEd25519ExportPub, + CompositeEd25519Sign, CompositeEd25519Verify, + TRAD_TYPE_ED25519 +}; +#endif /* !WOLFSSH_NO_ED25519 */ + +/* returns matching CompositeTradOps for tradType, NULL if unsupported */ +const CompositeTradOps* WS_GetTradOps(byte tradType) +{ + switch (tradType) { +#ifndef WOLFSSH_NO_ECDSA + case TRAD_TYPE_ECC: + return &compositeEccOps; +#endif +#ifndef WOLFSSH_NO_ED25519 + case TRAD_TYPE_ED25519: + return &compositeEd25519Ops; +#endif + default: + return NULL; + } +} + +/* mPrime scratch buffer size for VerifyMlDsaComposite/SignHMlDsaComposite */ +#define COMPOSITE_M_PRIME_SZ \ + (COMPOSITE_DOMAIN_PREFIX_SZ + COMPOSITE_MAX_LABEL_SZ + 1 + \ + WC_MAX_DIGEST_SIZE) + +/* returns WS_SUCCESS if sig verifies, negative WS_* error code otherwise */ +static int VerifyMlDsaComposite(byte keyId, void* heap, + MlDsaKey* mldsa, void* tradKey, + const byte* sig, word32 sigSz, + const byte* msg, word32 msgSz) +{ + int ret = WS_SUCCESS; + CompositeParams params; + int status = 0; + word32 mPrimeLen = 0; +#ifdef WOLFSSH_SMALL_STACK + byte* hash = NULL; + byte* mPrime = NULL; +#else + byte hash[WC_MAX_DIGEST_SIZE]; + byte mPrime[COMPOSITE_M_PRIME_SZ]; +#endif + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + if (params.tradHashSz > WC_MAX_DIGEST_SIZE) { + return WS_BUFFER_E; + } + + /* prevents "sigSz - mldsaSigSz" underflow below (sig/sigSz are + * wire-controlled) */ + if (sigSz < params.mldsaSigSz) { + return WS_KEY_FORMAT_E; + } + + /* ED25519/ED448 are fixed size; ECC's variable r/s is checked 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; + } + } + +#ifdef WOLFSSH_SMALL_STACK + hash = (byte*)WMALLOC(WC_MAX_DIGEST_SIZE, heap, DYNTYPE_TEMP); + if (hash == NULL) + ret = WS_MEMORY_E; + if (ret == WS_SUCCESS) { + mPrime = (byte*)WMALLOC(COMPOSITE_M_PRIME_SZ, heap, DYNTYPE_TEMP); + if (mPrime == NULL) + ret = WS_MEMORY_E; + } +#endif + + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + params.tradHashSz; + + if (ret == WS_SUCCESS) { + ret = WS_Hash_Helper(params.tradHashId, msg, msgSz, hash, params.tradHashSz); + if (ret != 0) ret = WS_CRYPTO_FAILED; + } + + if (ret == WS_SUCCESS) { + XMEMCPY(mPrime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); + mPrime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + } + + /* cheap trad verify first, so garbage sigs are rejected cheaply + * pre-auth */ + if (ret == WS_SUCCESS) { + const byte* tradSig = sig + params.mldsaSigSz; + word32 tradSigSz = sigSz - params.mldsaSigSz; + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->verify(tradKey, heap, + params.tradHashId, params.tradHashSz, + tradSig, tradSigSz, mPrime, mPrimeLen); + } + } + + if (ret == WS_SUCCESS) { + ret = wc_MlDsaKey_VerifyCtx(mldsa, + sig, params.mldsaSigSz, + (const byte*)params.label, (byte)params.labelSz, + mPrime, mPrimeLen, + &status); + if (ret != 0 || status != 1) { + WLOG(WS_LOG_DEBUG, "VerifyMlDsaComposite: ML-DSA Verify fail (%d, status=%d)", ret, status); + ret = WS_MLDSA_E; + } + } + +#ifdef WOLFSSH_SMALL_STACK + if (hash != NULL) { + wc_ForceZero(hash, WC_MAX_DIGEST_SIZE); + WFREE(hash, heap, DYNTYPE_TEMP); + } + if (mPrime != NULL) { + wc_ForceZero(mPrime, COMPOSITE_M_PRIME_SZ); + WFREE(mPrime, heap, DYNTYPE_TEMP); + } +#else + wc_ForceZero(hash, WC_MAX_DIGEST_SIZE); + wc_ForceZero(mPrime, COMPOSITE_M_PRIME_SZ); +#endif + + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int ParseMlDsaCompositePubKey(WOLFSSH* ssh, + struct wolfSSH_sigKeyBlock* sigKeyBlock_ptr, + byte* pubKey, word32 pubKeySz, byte keyId) +{ + int ret; + 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; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int SignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, + struct wolfSSH_sigKeyBlockFull *sigKey) +{ + int ret; + CompositeParams params; + word32 mPrimeLen = 0; + word32 mldsaSigSz; + byte keyId = sigKey->pubKeyId; +#ifdef WOLFSSH_SMALL_STACK + byte* hash = NULL; + byte* mPrime = NULL; +#else + byte hash[WC_MAX_DIGEST_SIZE]; + byte mPrime[COMPOSITE_M_PRIME_SZ]; +#endif + + WLOG(WS_LOG_DEBUG, "Entering SignHMlDsaComposite()"); + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + if (params.tradHashSz > WC_MAX_DIGEST_SIZE) { + return WS_BUFFER_E; + } + + mldsaSigSz = params.mldsaSigSz; + + /* verify sig buffer fits worst case before trad sign appends to it */ + if (*sigSz < (params.mldsaSigSz + params.tradSigSz)) { + return WS_BAD_ARGUMENT; + } + +#ifdef WOLFSSH_SMALL_STACK + hash = (byte*)WMALLOC(WC_MAX_DIGEST_SIZE, ssh->ctx->heap, DYNTYPE_TEMP); + if (hash == NULL) + ret = WS_MEMORY_E; + if (ret == WS_SUCCESS) { + mPrime = (byte*)WMALLOC(COMPOSITE_M_PRIME_SZ, ssh->ctx->heap, + DYNTYPE_TEMP); + if (mPrime == NULL) + ret = WS_MEMORY_E; + } +#endif + + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + params.tradHashSz; + + if (ret == WS_SUCCESS) { + ret = WS_Hash_Helper(params.tradHashId, ssh->h, ssh->hSz, hash, params.tradHashSz); + if (ret != 0) ret = WS_CRYPTO_FAILED; + } + + if (ret == WS_SUCCESS) { + XMEMCPY(mPrime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); + mPrime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + } + + if (ret == WS_SUCCESS) { + ret = wc_MlDsaKey_SignCtx(&sigKey->sk.mldsa_composite.mldsa, + (const byte*)params.label, + params.labelSz, + sig, &mldsaSigSz, mPrime, mPrimeLen, ssh->rng); + if (ret != 0 || mldsaSigSz != params.mldsaSigSz) { + WLOG(WS_LOG_DEBUG, "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, + mPrime, mPrimeLen, sig + params.mldsaSigSz, &wireSigSz); + if (ret == WS_SUCCESS) { + *sigSz = params.mldsaSigSz + wireSigSz; + } + else { + WLOG(WS_LOG_DEBUG, "SignHMlDsaComposite: trad sign fail (%d)", ret); + } + } + } + +#ifdef WOLFSSH_SMALL_STACK + if (hash != NULL) { + wc_ForceZero(hash, WC_MAX_DIGEST_SIZE); + WFREE(hash, ssh->ctx->heap, DYNTYPE_TEMP); + } + if (mPrime != NULL) { + wc_ForceZero(mPrime, COMPOSITE_M_PRIME_SZ); + WFREE(mPrime, ssh->ctx->heap, DYNTYPE_TEMP); + } +#else + wc_ForceZero(hash, WC_MAX_DIGEST_SIZE); + wc_ForceZero(mPrime, COMPOSITE_M_PRIME_SZ); +#endif + + WLOG(WS_LOG_DEBUG, "Leaving SignHMlDsaComposite(), ret = %d", ret); + return ret; +} + +static int PrepareUserAuthRequestMlDsaComposite(WOLFSSH* ssh, word32* payloadSz, + const WS_UserAuthData* authData, WS_KeySignature* keySig) +{ + int ret = WS_SUCCESS; + CompositeParams params; + byte keyId; + + WLOG(WS_LOG_DEBUG, "Entering PrepareUserAuthRequestMlDsaComposite()"); + if (ssh == NULL || payloadSz == NULL || authData == NULL || keySig == NULL) + ret = WS_BAD_ARGUMENT; + + if (ret == WS_SUCCESS) { + keyId = keySig->keyId; + ret = WS_GetCompositeParams(keyId, ¶ms); + } + + if (ret == WS_SUCCESS) { + word32 idx = 0; + + /* composite keys are OpenSSH-format only; GetOpenSshKey() also + * self-initializes keySig, so no pre-init needed */ + ret = GetOpenSshKey(keySig, + authData->sf.publicKey.privateKey, + authData->sf.publicKey.privateKeySz, &idx); + if (ret == WS_SUCCESS && keySig->keyId != keyId) { + wolfSSH_KEY_clean(keySig); + keySig->keyId = ID_NONE; + ret = WS_KEY_FORMAT_E; + } + } + + if (ret == WS_SUCCESS) { + if (authData->sf.publicKey.hasSignature) { + word32 sigSz = params.mldsaSigSz + params.tradSigSz; + *payloadSz += (LENGTH_SZ * 3) + sigSz + authData->sf.publicKey.publicKeyTypeSz; + keySig->sigSz = sigSz; + } + } + + WLOG(WS_LOG_DEBUG, "Leaving PrepareUserAuthRequestMlDsaComposite(), ret = %d", ret); + return ret; +} + +static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + byte* output, word32* idx, + const WS_UserAuthData* authData, + const byte* sigStart, word32 sigStartIdx, + WS_KeySignature* keySig) +{ + word32 begin; + int ret = WS_SUCCESS; + byte* sig = NULL; + word32 sigSz; + byte* checkData = NULL; + word32 checkDataSz = 0; + byte* hash = NULL; + byte* mPrime = NULL; + word32 mldsaSigSz; + word32 mPrimeLen; + CompositeParams params; + byte keyId; + + WLOG(WS_LOG_DEBUG, "Entering BuildUserAuthRequestMlDsaComposite()"); + if (ssh == NULL || output == NULL || idx == NULL || authData == NULL || + sigStart == NULL || keySig == NULL) { + return WS_BAD_ARGUMENT; + } + keyId = keySig->keyId; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + mldsaSigSz = params.mldsaSigSz; + sigSz = (word32)keySig->sigSz; + + /* sigSz is already the worst-case total; slack below isn't load-bearing */ + sig = (byte*)WMALLOC(sigSz + COMPOSITE_SIG_ALLOC_SLACK_SZ, + keySig->heap, DYNTYPE_BUFFER); + if (sig == NULL) + ret = WS_MEMORY_E; + + begin = *idx; + + if (ret == WS_SUCCESS) { + checkDataSz = LENGTH_SZ + ssh->sessionIdSz + (begin - sigStartIdx); + checkData = (byte*)WMALLOC(checkDataSz, keySig->heap, DYNTYPE_TEMP); + if (checkData == NULL) + ret = WS_MEMORY_E; + } + + if (ret == WS_SUCCESS) { + word32 i = 0; + + c32toa(ssh->sessionIdSz, checkData + i); + i += LENGTH_SZ; + WMEMCPY(checkData + i, ssh->sessionId, ssh->sessionIdSz); + i += ssh->sessionIdSz; + WMEMCPY(checkData + i, sigStart, begin - sigStartIdx); + } + + if (ret == WS_SUCCESS) { + hash = (byte*)WMALLOC(params.tradHashSz, keySig->heap, DYNTYPE_TEMP); + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + params.tradHashSz; + mPrime = (byte*)WMALLOC(mPrimeLen, keySig->heap, DYNTYPE_TEMP); + + if (hash == NULL || mPrime == NULL) { + ret = WS_MEMORY_E; + } + } + + if (ret == WS_SUCCESS) { + ret = WS_Hash_Helper(params.tradHashId, checkData, checkDataSz, hash, params.tradHashSz); + if (ret != 0) { + ret = WS_CRYPTO_FAILED; + } + } + + if (ret == WS_SUCCESS) { + XMEMCPY(mPrime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); + mPrime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + } + + if (ret == WS_SUCCESS) { + WLOG(WS_LOG_INFO, "Signing with hybrid composite (ML-DSA component)."); + ret = wc_MlDsaKey_SignCtx(&keySig->ks.mldsa_composite.mldsa, + (const byte*)params.label, + params.labelSz, + sig, &mldsaSigSz, mPrime, mPrimeLen, ssh->rng); + if (ret != 0 || mldsaSigSz != params.mldsaSigSz) { + WLOG(WS_LOG_DEBUG, "BUARMlDsaComposite: ML-DSA sign fail (%d)", ret); + ret = WS_MLDSA_E; + } + } + + if (ret == WS_SUCCESS) { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + word32 wireSigSz = params.tradSigSz; + WLOG(WS_LOG_INFO, "Signing with hybrid composite (trad component)."); + ret = ops->sign(&keySig->ks.mldsa_composite.trad, ssh->rng, + keySig->heap, params.tradHashId, params.tradHashSz, + mPrime, mPrimeLen, sig + params.mldsaSigSz, &wireSigSz); + if (ret == WS_SUCCESS) { + sigSz = params.mldsaSigSz + wireSigSz; + } + else { + WLOG(WS_LOG_DEBUG, "BUARMlDsaComposite: trad sign fail (%d)", ret); + } + } + } + + if (ret == WS_SUCCESS) { + c32toa(LENGTH_SZ * 2 + authData->sf.publicKey.publicKeyTypeSz + sigSz, + output + begin); + begin += LENGTH_SZ; + + c32toa(authData->sf.publicKey.publicKeyTypeSz, output + begin); + begin += LENGTH_SZ; + WMEMCPY(output + begin, authData->sf.publicKey.publicKeyType, authData->sf.publicKey.publicKeyTypeSz); + begin += authData->sf.publicKey.publicKeyTypeSz; + + c32toa(sigSz, output + begin); + begin += LENGTH_SZ; + WMEMCPY(output + begin, sig, sigSz); + begin += sigSz; + } + + if (ret == WS_SUCCESS) + *idx = begin; + + if (sig != NULL) { + WS_FORCEZERO(sig, sigSz); + WFREE(sig, keySig->heap, DYNTYPE_BUFFER); + } + if (checkData != NULL) { + WS_FORCEZERO(checkData, checkDataSz); + WFREE(checkData, keySig->heap, DYNTYPE_TEMP); + } + if (hash != NULL) { + WS_FORCEZERO(hash, params.tradHashSz); + WFREE(hash, keySig->heap, DYNTYPE_TEMP); + } + if (mPrime != NULL) { + wc_ForceZero(mPrime, mPrimeLen); + WFREE(mPrime, keySig->heap, DYNTYPE_TEMP); + } + + WLOG(WS_LOG_DEBUG, "Leaving BuildUserAuthRequestMlDsaComposite(), ret = %d", ret); + return ret; +} + +static int DoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + WS_UserAuthData_PublicKey* pk, WS_UserAuthData* authData, + byte keyId, word32 pubKeyBlobSz) +{ + const byte* publicKeyType = NULL; + word32 publicKeyTypeSz = 0; + word32 pubRawSz = 0; + word32 sigSz = 0; + word32 i = 0; + int ret = WS_SUCCESS; + CompositeParams params; + WS_KeySignature* keySig = NULL; + + WLOG(WS_LOG_DEBUG, "Entering DoUserAuthRequestMlDsaComposite()"); + + if (ssh == NULL || ssh->ctx == NULL || pk == NULL || authData == NULL) { + return WS_BAD_ARGUMENT; + } + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + keySig = (WS_KeySignature*)WMALLOC(sizeof(WS_KeySignature), ssh->ctx->heap, DYNTYPE_PUBKEY); + if (keySig == NULL) { + ret = WS_MEMORY_E; + } + else { + XMEMSET(keySig, 0, sizeof(*keySig)); + keySig->keyId = keyId; + keySig->heap = ssh->ctx->heap; + } + + if (ret == WS_SUCCESS) { + int mldsaInit = 0; + int tradInit = 0; + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + + ret = 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); + + WS_FORCEZERO(checkData, idx + dataToSignSz); + WFREE(checkData, ssh->ctx->heap, DYNTYPE_TEMP); + } + } + + wc_MlDsaKey_Free(&keySig->ks.mldsa_composite.mldsa); + { + const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + if (ops != NULL) { + ops->free(&keySig->ks.mldsa_composite.trad); + } + } + WFREE(keySig, ssh->ctx->heap, DYNTYPE_PUBKEY); + } + + return ret; +} +#endif + +#ifdef WOLFSSH_TEST_INTERNAL + +#ifndef WOLFSSH_NO_MLDSA +int wolfSSH_TestDoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + WS_UserAuthData* authData, byte keyId, word32 pubKeyBlobSz) +{ + if (authData == NULL) + return WS_BAD_ARGUMENT; + + return DoUserAuthRequestMlDsaComposite(ssh, &authData->sf.publicKey, authData, + keyId, pubKeyBlobSz); +} + +int wolfSSH_TestPrepareUserAuthRequestMlDsaComposite(WOLFSSH* ssh, + word32* payloadSz, const WS_UserAuthData* authData, + WS_KeySignature* keySig) +{ + return PrepareUserAuthRequestMlDsaComposite(ssh, payloadSz, authData, keySig); +} + +/* exercises SignHMlDsaComposite() with a throwaway keypair; returns + * WS_SUCCESS or negative WS_* error code */ +int wolfSSH_TestSignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, + byte keyId) +{ + int ret; + CompositeParams params; + struct wolfSSH_sigKeyBlockFull sigKey; + const CompositeTradOps* ops; + + if (ssh == NULL || sig == NULL || sigSz == NULL) + return WS_BAD_ARGUMENT; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) + return ret; + + ops = WS_GetTradOps(params.tradType); + + WMEMSET(&sigKey, 0, sizeof(sigKey)); + sigKey.pubKeyId = keyId; + + ret = wc_MlDsaKey_Init(&sigKey.sk.mldsa_composite.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 not in CompositeTradOps: only this one call site needs it */ + if (ret == 0) { + if (params.tradType == TRAD_TYPE_ED25519) { +#ifndef WOLFSSH_NO_ED25519 + ret = wc_ed25519_make_key(ssh->rng, ED25519_KEY_SIZE, + &sigKey.sk.mldsa_composite.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..1d4dacd9c 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,326 @@ int wolfSSH_MakeMlDsaKey(byte* out, word32 outSz, word32 level) #endif } + +/* builds an OpenSSH-key-v1 envelope (the only on-disk form for composite + * keys, which have no ASN.1 representation); see + * GetOpenSshKeyMlDsaComposite() in src/internal.c for the parser. + * 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 } + * pub is (mldsaPub || tradPub), 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); + + /* final base64 size vs. outSz is checked below, after PEM body build */ + compositePubSz = params.mldsaPubSz + params.tradPubSz; + compositePrivSz = MLDSA_SEED_SZ + params.tradPrivSz; + + pubBlobSz = UINT32_SZ + keyTypeNameSz + UINT32_SZ + compositePubSz; + privKeysStrSz = UINT32_SZ * 2 /* checkints */ + + UINT32_SZ + keyTypeNameSz + + UINT32_SZ + compositePubSz + + UINT32_SZ + compositePrivSz + + UINT32_SZ /* comment (empty) */; + padSz = (MIN_BLOCK_SZ - (privKeysStrSz % MIN_BLOCK_SZ)) % MIN_BLOCK_SZ; + privKeysStrSz += padSz; + + fileSz = (word32)WSTRLEN(magic) + 1 + + UINT32_SZ + noneSz /* ciphername */ + + UINT32_SZ + noneSz /* kdfname */ + + UINT32_SZ /* kdfoptions (empty) */ + + UINT32_SZ /* keycount */ + + UINT32_SZ + pubBlobSz + + UINT32_SZ + privKeysStrSz; + + 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 != WC_NO_ERR_TRACE(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); + + WS_FORCEZERO(mldsaSeed, sizeof(mldsaSeed)); + WS_FORCEZERO(tradPriv, sizeof(tradPriv)); + + if (tmpBuf != NULL) { + WS_FORCEZERO(tmpBuf, fileSz); + WFREE(tmpBuf, NULL, DYNTYPE_BUFFER); + } + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_MakeMlDsaCompositeKey(), ret = %d", + ret); + return ret; +#else + WOLFSSH_UNUSED(out); + WOLFSSH_UNUSED(outSz); + WOLFSSH_UNUSED(level); + WOLFSSH_UNUSED(tradType); + return WS_NOT_COMPILED; +#endif +} + #else /* WOLFSSL_KEY_GEN */ #error "wolfSSH keygen requires that keygen is enabled in wolfSSL, use --enable-keygen or #define WOLFSSL_KEY_GEN." #endif /* WOLFSSL_KEY_GEN */ diff --git a/src/ossh.c b/src/ossh.c index faf2a68e9..419e63f6f 100644 --- a/src/ossh.c +++ b/src/ossh.c @@ -297,6 +297,78 @@ static int GetOpenSshKeyMlDsa(MlDsaKey* key, } return ret; } + +/* Parse OpenSSH ML-DSA composite private key blob; see + * GetOpenSshKeyPublicMlDsaComposite() for the public-key-only counterpart. + * Returns WS_SUCCESS or negative WS_* error. */ +static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, + void* tradKey, void* heap, const byte* buf, word32 len, word32* idx) +{ + const byte *pub = NULL; + const byte *priv = NULL; + word32 pubSz = 0; + word32 privSz = 0; + int ret; + CompositeParams params; + const CompositeTradOps* ops; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + ops = WS_GetTradOps(params.tradType); + + /* caller doesn't clean up on error; each path below frees what it + * already initialized */ + ret = wc_MlDsaKey_Init(mldsa, heap, INVALID_DEVID); + if (ret != 0) { + return WS_CRYPTO_FAILED; + } + 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; +} #endif #ifdef WOLFSSH_TPM @@ -357,6 +429,64 @@ static int GetOpenSshKeyPublicMlDsa(MlDsaKey* key, const byte* buf, } return ret; } + +/* public-key-only counterpart to GetOpenSshKeyMlDsaComposite(); no trailing + * private-key string to parse. Returns WS_SUCCESS or negative WS_* error */ +static int GetOpenSshKeyPublicMlDsaComposite(byte keyId, MlDsaKey* mldsa, + void* tradKey, void* heap, const byte* buf, word32 len, word32* idx) +{ + int ret; + int mldsaInit = 0; + int tradInit = 0; + const byte* pub = NULL; + word32 pubSz = 0; + CompositeParams params; + const CompositeTradOps* ops; + + ret = WS_GetCompositeParams(keyId, ¶ms); + if (ret != WS_SUCCESS) return ret; + + ops = WS_GetTradOps(params.tradType); + + ret = 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, @@ -423,6 +553,12 @@ int GetOpenSshPublicKey(WS_KeySignature *key, ret = GetOpenSshKeyPublicMlDsa(&key->ks.mldsa.key, buf, len, idx, WC_ML_DSA_87); break; + 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; @@ -532,16 +668,36 @@ int GetOpenSshKey(WS_KeySignature *key, ret = GetOpenSshKeyMlDsa( &key->ks.mldsa.key, str, strSz, &subIdx, WC_ML_DSA_44); + /* clear keyId: key already freed, avoid + * double free */ + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; break; case ID_MLDSA65: ret = GetOpenSshKeyMlDsa( &key->ks.mldsa.key, str, strSz, &subIdx, WC_ML_DSA_65); + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; break; case ID_MLDSA87: ret = GetOpenSshKeyMlDsa( &key->ks.mldsa.key, str, strSz, &subIdx, WC_ML_DSA_87); + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; + break; + case ID_MLDSA44_ED25519: + ret = GetOpenSshKeyMlDsaComposite( + key->keyId, + &key->ks.mldsa_composite.mldsa, + &key->ks.mldsa_composite.trad, + key->heap, + str, strSz, &subIdx); + /* clear keyId: key already freed, avoid + * double free */ + if (ret != WS_SUCCESS) + key->keyId = ID_NONE; break; #endif default: @@ -567,7 +723,9 @@ int GetOpenSshKey(WS_KeySignature *key, check1 <= check2; check1++, subIdx++) { if (check1 != str[subIdx]) { - /* Bad pad value. */ + /* bad pad: free key decoded above */ + wolfSSH_KEY_clean(key); + key->keyId = ID_NONE; ret = WS_KEY_FORMAT_E; break; } diff --git a/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 8861f87b2..704bf9927 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -901,6 +901,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) { @@ -924,6 +935,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 @@ -8225,6 +8366,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) { @@ -8534,6 +9514,10 @@ static int test_IdentifyAsn1Key(void) ret = IdentifyAsn1Key(unitTestMlDsaPrivKey, (word32)sizeof(unitTestMlDsaPrivKey), 1, NULL, NULL); + /* unitTestMlDsaPrivKey encodes a level-44 key; the private-key decode + * path determines keyId purely from the decoded key's level (see + * IdentifyAsn1Key), not from WOLFSSH_NO_MLDSA44, so this must always + * be ID_MLDSA44 regardless of which individual levels are compiled in. */ if (ret != ID_MLDSA44) { printf("IdentifyAsn1Key: MlDsa priv failed, ret=%d\n", ret); result = -606; @@ -11990,7 +12974,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); @@ -12205,6 +13205,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 267319053..dfdd5b89c 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -39,6 +39,9 @@ #include #include #include +#ifdef HAVE_ED448 +#include +#endif #ifndef WOLFSSL_WOLFSSH #error "wolfssh requires wolfSSL built with WOLFSSL_WOLFSSH" @@ -458,6 +461,8 @@ enum { ID_MLDSA44, ID_MLDSA65, ID_MLDSA87, + /* always declared; NameIdMap/WS_GetCompositeParams() gate reachability */ + ID_MLDSA44_ED25519, #endif ID_X509V3_SSH_RSA, ID_X509V3_ECDSA_SHA2_NISTP256, @@ -1277,9 +1282,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) + /* keep union non-empty (empty union rejected by some + * compilers) though unusable without a trad 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 trad public key size: P-384 uncompressed point */ +#define COMPOSITE_MAX_TRAD_PUB_SZ (1 + (2 * ECC_P384_COORD_SZ)) +/* worst-case trad private key size: Ed448 seed, else P-384 scalar */ +#ifdef HAVE_ED448 +#define COMPOSITE_MAX_TRAD_PRIV_SZ ED448_KEY_SIZE +#else +#define COMPOSITE_MAX_TRAD_PRIV_SZ ECC_P384_COORD_SZ +#endif +/* worst-case trad signature size: Ed448, else P-384 raw r/s */ +#ifdef HAVE_ED448 +#define COMPOSITE_MAX_TRAD_SIG_SZ ED448_SIG_SIZE +#else +#define COMPOSITE_MAX_TRAD_SIG_SZ (2 * ECC_P384_COORD_SZ) +#endif +/* defensive slack on top of BuildUserAuthRequestMlDsaComposite()'s + * worst-case signature size; not load-bearing */ +#define COMPOSITE_SIG_ALLOC_SLACK_SZ 32 + +typedef struct CompositeParams { + const char* label; + word32 mldsaSigSz; + word32 mldsaPubSz; + word32 tradHashSz; + word32 labelSz; + word32 tradPubSz; + word32 tradSigSz; + word32 tradPrivSz; + enum wc_HashType tradHashId; + byte keyId; + byte mldsaLevel; + byte tradType; +} CompositeParams; + +/* dispatch table for a composite key's trad (ECC/Ed25519/Ed448) half; see + * WS_GetTradOps() in src/internal.c */ +typedef struct CompositeTradOps { + int (*init)(void* key, void* heap); + void (*free)(void* key); + int (*importPub)(void* key, const byte* pub, word32 pubSz); + int (*importPriv)(void* key, const byte* priv, word32 privSz, + const byte* pub, word32 pubSz); + int (*exportPub)(void* key, byte* out, word32* outSz); + int (*sign)(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* mPrime, word32 mPrimeLen, + byte* wireSig, word32* wireSigSz); + int (*verify)(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* mPrime, word32 mPrimeLen); + byte tradType; +} CompositeTradOps; + +WOLFSSH_LOCAL int WS_GetCompositeParams(byte keyId, CompositeParams* params); +WOLFSSH_LOCAL const CompositeTradOps* WS_GetTradOps(byte tradType); +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); @@ -1723,6 +1822,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 3d78c134d2fba2330465fa71a659bb8c14ed2476 Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Thu, 23 Jul 2026 09:24:30 -0600 Subject: [PATCH 2/2] Add remaining ML-DSA composite signature algorithms --- apps/wolfsshd/auth.c | 41 +- apps/wolfsshd/auth.h | 7 +- apps/wolfsshd/test/test_configuration.c | 267 +++++++- apps/wolfsshd/wolfsshd.c | 5 +- examples/echoserver/echoserver.c | 129 +++- keys/include.am | 6 +- keys/server-key-mldsa44.pem | 83 +++ 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 | 855 +++++++++++++++++------- src/keygen.c | 296 ++++---- src/ossh.c | 58 +- src/ssh.c | 49 +- tests/kex.c | 26 +- tests/unit.c | 467 +++++++++++-- wolfssh/error.h | 3 +- wolfssh/internal.h | 72 +- wolfssh/keygen.h | 9 +- 21 files changed, 1786 insertions(+), 587 deletions(-) create mode 100644 keys/server-key-mldsa44.pem 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 43918ffdd..a84602ba8 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -153,8 +153,8 @@ struct WOLFSSHD_AUTH { #ifndef WOLFSSH_NO_MLDSA #ifndef WOLFSSH_NO_MLDSA87 #if defined(WOLFSSH_CERTS) - /* x509v3-ssh-mldsa-87: ML-DSA-87 pubkey + CA sig + DER - * overhead, base64-encoded */ + /* x509v3-ssh-mldsa-87 size (pubkey+CA sig+DER, base64); + * COMPOSITE_MAX_TRAD_PUB_SZ is headroom for a future variant. */ #define MAX_LINE_SZ \ ((WC_MLDSA_87_PUB_KEY_SIZE + WC_MLDSA_87_SIG_SIZE + \ COMPOSITE_MAX_TRAD_PUB_SZ + 1024 + 2) / 3 * 4 + 640) @@ -189,6 +189,15 @@ struct WOLFSSHD_AUTH { #endif #endif +#ifdef WOLFSSHD_UNIT_TEST +/* Exposes MAX_LINE_SZ so tests can size worst-case lines without + * duplicating the formula above. */ +word32 wolfsshd_test_MaxLineSz(void) +{ + return (word32)MAX_LINE_SZ; +} +#endif + #if 0 /* this could potentially be useful in a deeply embedded future port */ @@ -285,10 +294,31 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, #endif #endif #endif - #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + "ssh-mldsa44-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384", + #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) "ssh-mldsa44-ed25519@openssh.com", #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa65-ed25519", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448", + #endif }; + const int NUM_ALLOWED_TYPES = + (int)(sizeof(allowedTypes) / sizeof(allowedTypes[0])); int typeOk = 0; int i; @@ -305,9 +335,8 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, } } if (ret == WSSHD_AUTH_SUCCESS) { - for (i = 0; i < (int)(sizeof(allowedTypes) / sizeof(allowedTypes[0])); - ++i) { - if (allowedTypes[i] != NULL && WSTRCMP(type, allowedTypes[i]) == 0) { + for (i = 0; i < NUM_ALLOWED_TYPES; ++i) { + if (WSTRCMP(type, allowedTypes[i]) == 0) { typeOk = 1; break; } diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 848401e6b..8f28e7d8c 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -27,6 +27,8 @@ #define WOLFSSH_USER_GET_STRING(x) #x #define WOLFSSH_USER_STRING(x) WOLFSSH_USER_GET_STRING(x) +#include /* for wc_KeyPemToDer */ + #if 0 typedef struct USER_NODE USER_NODE; @@ -101,7 +103,9 @@ int wolfSSHD_GetHomeDirectory(WOLFSSHD_AUTH* auth, WOLFSSH* ssh, WCHAR* out, int int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid, int rejectReadable, void* heap, WFILE** out); -/* classifies a loaded host private key buffer as OpenSSH or ASN1/DER */ +/* classifies a loaded host private key buffer as OpenSSH or ASN1/DER. + * *keyDer is a WMALLOC'd (heap, DYNTYPE_SSHD) buffer to WS_FORCEZERO + + * WFREE on a PEM decode, else NULL. */ int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, void* heap, byte** keyDer, byte** privBuf, word32* privBufSz); @@ -121,6 +125,7 @@ int SearchForPubKey(const char* path, const char* authKeysFile, const char* user, const WS_UserAuthData_PublicKey* pubKeyCtx, WUID_T uid, int strictModes); +word32 wolfsshd_test_MaxLineSz(void); #endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) int CheckPasswordHashUnix(const char* input, char* stored); diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 06c250efd..5ff719e90 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -1440,8 +1440,8 @@ static int BuildAuthKeysLine(const byte* key, word32 keySz, } /* 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. */ + * is recognized, guarding against the table and its recognition logic + * drifting out of sync as the ML-DSA/composite/cert #ifdef branches change. */ static int test_CheckAuthKeysLineTypes(void) { static const char* types[] = { @@ -1478,9 +1478,28 @@ static int test_CheckAuthKeysLineTypes(void) #endif #endif #endif - #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + "ssh-mldsa44-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384", + #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) "ssh-mldsa44-ed25519@openssh.com", #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa65-ed25519", + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448", + #endif }; static const char keyAStr[] = "wolfssh-auth-key-test-A-AAAAAAA"; static const char keyBStr[] = "wolfssh-auth-key-test-B-BBBBBBB"; @@ -1515,7 +1534,8 @@ static int test_CheckAuthKeysLineTypes(void) } } - /* An unknown type must be rejected with a negative status. */ + /* An unknown type must be skipped (not matched) rather than aborting + * the whole authorized_keys scan with a fatal error. */ ret = BuildAuthKeysLineType("ssh-bogus-type", keyA, keySz, line, sizeof(line)); if (ret != WS_SUCCESS) { @@ -1524,7 +1544,7 @@ static int test_CheckAuthKeysLineTypes(void) Log(" Testing scenario: unknown type is rejected."); WMEMCPY(lineCopy, line, WSTRLEN(line) + 1); rc = CheckAuthKeysLine(lineCopy, (word32)WSTRLEN(lineCopy), keyA, keySz); - if (rc < 0) { + if (rc == WSSHD_AUTH_FAILURE) { Log(" PASSED.\n"); } else { @@ -1535,6 +1555,143 @@ static int test_CheckAuthKeysLineTypes(void) return WS_SUCCESS; } +#ifndef WOLFSSH_NO_MLDSA +/* Builds a worst-case " " line, confirms it fits + * within MAX_LINE_SZ, and round-trips it through CheckAuthKeysLine(). */ +static int CheckAuthKeysLineMaxSzCase(const char* type, word32 keySz, + word32 maxLineSz) +{ + int ret = WS_SUCCESS; + int rc; + word32 lineBufSz = maxLineSz + 1; + byte* key = NULL; + char* line = NULL; + char* lineCopy = NULL; + + key = (byte*)WMALLOC(keySz, NULL, DYNTYPE_BUFFER); + line = (char*)WMALLOC(lineBufSz, NULL, DYNTYPE_BUFFER); + lineCopy = (char*)WMALLOC(lineBufSz, NULL, DYNTYPE_BUFFER); + if (key == NULL || line == NULL || lineCopy == NULL) { + ret = WS_MEMORY_E; + } + + if (ret == WS_SUCCESS) { + word32 i; + /* Non-repeating pattern so a truncation bug shows up as a + * mismatch, not accidental luck. */ + for (i = 0; i < keySz; i++) { + key[i] = (byte)(i * 31 + 7); + } + + ret = BuildAuthKeysLineType(type, key, keySz, line, lineBufSz); + } + + if (ret == WS_SUCCESS) { + word32 lineLen = (word32)WSTRLEN(line); + + Log(" Testing scenario: max-size %s (%u byte key, %u byte line) " + "fits within MAX_LINE_SZ (%u) and round-trips.", + type, keySz, lineLen, maxLineSz); + if (lineLen + 1 > maxLineSz) { + Log(" FAILED (line len %u exceeds MAX_LINE_SZ %u).\n", + lineLen + 1, maxLineSz); + ret = WS_FATAL_ERROR; + } + else { + WMEMCPY(lineCopy, line, lineLen + 1); + rc = CheckAuthKeysLine(lineCopy, lineLen, key, keySz); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED (rc=%d).\n", rc); + ret = WS_FATAL_ERROR; + } + } + } + + if (key != NULL) { + WFREE(key, NULL, DYNTYPE_BUFFER); + } + if (line != NULL) { + WFREE(line, NULL, DYNTYPE_BUFFER); + } + if (lineCopy != NULL) { + WFREE(lineCopy, NULL, DYNTYPE_BUFFER); + } + + return ret; +} + +/* MAX_LINE_SZ is sized off the largest ML-DSA level, not the 32-byte + * dummy keys test_CheckAuthKeysLineTypes() uses; build a full-size key + * to actually catch a miscalculation there. */ +static int test_CheckAuthKeysLineMaxSz(void) +{ + int ret; + const char* type; + word32 keySz; + word32 maxLineSz = wolfsshd_test_MaxLineSz(); + +#if !defined(WOLFSSH_NO_MLDSA87) + keySz = WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ; + #if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + type = "ssh-mldsa87-es384"; + #elif defined(HAVE_ED448) + type = "ssh-mldsa87-ed448"; + #else + type = "ssh-mldsa-87"; + keySz = WC_MLDSA_87_PUB_KEY_SIZE; + #endif +#elif !defined(WOLFSSH_NO_MLDSA65) + keySz = WC_MLDSA_65_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ; + #if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + type = "ssh-mldsa65-es256"; + #elif !defined(WOLFSSH_NO_ED25519) && !defined(NO_SHA512) + type = "ssh-mldsa65-ed25519"; + #else + type = "ssh-mldsa-65"; + keySz = WC_MLDSA_65_PUB_KEY_SIZE; + #endif +#else + keySz = WC_MLDSA_44_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ; + #if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + type = "ssh-mldsa44-es256"; + #elif !defined(WOLFSSH_NO_ED25519) && !defined(NO_SHA512) + type = "ssh-mldsa44-ed25519@openssh.com"; + #else + type = "ssh-mldsa-44"; + keySz = WC_MLDSA_44_PUB_KEY_SIZE; + #endif +#endif + + ret = CheckAuthKeysLineMaxSzCase(type, keySz, maxLineSz); + +#if defined(WOLFSSH_CERTS) + /* WOLFSSH_CERTS adds MAX_LINE_SZ headroom for x509v3 composite-cert + * lines; cover that branch too, not just the plain-pubkey case. */ + if (ret == WS_SUCCESS) { + #if !defined(WOLFSSH_NO_MLDSA87) + type = "x509v3-ssh-mldsa-87"; + keySz = WC_MLDSA_87_PUB_KEY_SIZE + WC_MLDSA_87_SIG_SIZE + + COMPOSITE_MAX_TRAD_PUB_SZ + 1024; + #elif !defined(WOLFSSH_NO_MLDSA65) + type = "x509v3-ssh-mldsa-65"; + keySz = WC_MLDSA_65_PUB_KEY_SIZE + WC_MLDSA_65_SIG_SIZE + + COMPOSITE_MAX_TRAD_PUB_SZ + 1024; + #else + type = "x509v3-ssh-mldsa-44"; + keySz = WC_MLDSA_44_PUB_KEY_SIZE + WC_MLDSA_44_SIG_SIZE + + COMPOSITE_MAX_TRAD_PUB_SZ + 1024; + #endif + ret = CheckAuthKeysLineMaxSzCase(type, keySz, maxLineSz); + } +#endif /* WOLFSSH_CERTS */ + + return ret; +} +#endif /* !WOLFSSH_NO_MLDSA */ + /* Negative-path coverage for CheckAuthKeysLine so mutation of the * ConstantCompare clause (the only substantive bytewise check after the * length comparison) does not survive the test suite. */ @@ -3882,21 +4039,28 @@ static int BuildKeyPath(const char* name, char* out, size_t outSz) /* 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. */ + * keys, which are only ever stored in that raw form), PKCS#8 PEM keys + * (e.g. ML-DSA), and traditional PEM/DER keys. */ static int test_DetectPrivKeyFormat(void) { typedef struct { const char* desc; const char* file; int wantFormat; + /* Expected wc_KeyPemToDer() outcome (1 = succeeds, keyDer non-NULL; + * 0 = fails, keyDer NULL); pins down which branch each case hits. */ + int wantKeyDerNonNull; } DPK_CASE; static const DPK_CASE cases[] = { - { "PEM-armored OpenSSH key", "id_ecdsa", WOLFSSH_FORMAT_OPENSSH }, + { "PEM-armored OpenSSH key", "id_ecdsa", WOLFSSH_FORMAT_OPENSSH, 0 }, { "raw binary openssh-key-v1 composite ML-DSA key", - "server-key-mldsa44ed25519", WOLFSSH_FORMAT_OPENSSH }, + "server-key-mldsa44ed25519", WOLFSSH_FORMAT_OPENSSH, 0 }, { "PEM traditional key decodes to DER/ASN1", "server-key-ecc.pem", - WOLFSSH_FORMAT_ASN1 }, + WOLFSSH_FORMAT_ASN1, 1 }, + { "un-armored raw DER key falls through to ASN1", + "server-key-mldsa44.der", WOLFSSH_FORMAT_ASN1, 0 }, + { "PKCS#8 PEM ML-DSA key decodes to DER/ASN1 without PKCS8 " + "stripping", "server-key-mldsa44.pem", WOLFSSH_FORMAT_ASN1, 1 }, }; word32 i; int ret = WS_SUCCESS; @@ -3908,14 +4072,19 @@ static int test_DetectPrivKeyFormat(void) /* 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. */ + * getBufferFromFile() can hand back. Poison out-params with sentinels + * first, so the NULL/0 checks below catch a skipped reset instead of + * matching by coincidence. */ + badKeyDer = (byte*)&dummy; + badPrivBuf = (byte*)&dummy; + badPrivBufSz = 0xDEADBEEF; badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, 0, NULL, &badKeyDer, &badPrivBuf, &badPrivBufSz); Log(" Testing scenario: 0-length buffer. %s\n", - (badGot == WS_BAD_ARGUMENT && badPrivBuf == NULL && badPrivBufSz == 0) - ? "PASSED" : "FAILED"); - if (badGot != WS_BAD_ARGUMENT || badPrivBuf != NULL || - badPrivBufSz != 0) { + (badGot == WS_BAD_ARGUMENT && badKeyDer == NULL && + badPrivBuf == NULL && badPrivBufSz == 0) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT || badKeyDer != NULL || + badPrivBuf != NULL || badPrivBufSz != 0) { return WS_FATAL_ERROR; } @@ -3927,6 +4096,30 @@ static int test_DetectPrivKeyFormat(void) return WS_FATAL_ERROR; } + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, sizeof(dummy), NULL, NULL, + &badPrivBuf, &badPrivBufSz); + Log(" Testing scenario: NULL keyDer pointer. %s\n", + (badGot == WS_BAD_ARGUMENT) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT) { + return WS_FATAL_ERROR; + } + + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, sizeof(dummy), NULL, + &badKeyDer, NULL, &badPrivBufSz); + Log(" Testing scenario: NULL privBuf pointer. %s\n", + (badGot == WS_BAD_ARGUMENT) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT) { + return WS_FATAL_ERROR; + } + + badGot = wolfSSHD_DetectPrivKeyFormat(&dummy, sizeof(dummy), NULL, + &badKeyDer, &badPrivBuf, NULL); + Log(" Testing scenario: NULL privBufSz pointer. %s\n", + (badGot == WS_BAD_ARGUMENT) ? "PASSED" : "FAILED"); + if (badGot != WS_BAD_ARGUMENT) { + return WS_FATAL_ERROR; + } + for (i = 0; i < (word32)(sizeof(cases) / sizeof(cases[0])); i++) { char path[128]; byte* data; @@ -3958,6 +4151,14 @@ static int test_DetectPrivKeyFormat(void) ret = WS_FATAL_ERROR; } + Log(" Testing scenario: %s wc_KeyPemToDer branch. %s\n", + cases[i].desc, + ((keyDer != NULL) == (cases[i].wantKeyDerNonNull != 0)) ? + "PASSED" : "FAILED"); + if ((keyDer != NULL) != (cases[i].wantKeyDerNonNull != 0)) { + ret = WS_FATAL_ERROR; + } + if (keyDer != NULL) { WFREE(keyDer, NULL, DYNTYPE_SSHD); } @@ -3967,6 +4168,39 @@ static int test_DetectPrivKeyFormat(void) } } + /* Synthetic case: wc_KeyPemToDer() succeeds but the decoded body starts + * with the openssh-key-v1 magic -- forces the "PEM-decoded result may + * still be OpenSSH binary" path no file-based case above reaches. */ + { + /* base64 of "openssh-key-v1\0PADPADPADPADPAD" */ + static const char pemOpenSshBody[] = + "-----BEGIN PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAUEFEUEFEUEFEUEFEUEFE\n" + "-----END PRIVATE KEY-----\n"; + byte* keyDer = NULL; + byte* privBuf = NULL; + word32 privBufSz = 0; + int gotFormat; + + gotFormat = wolfSSHD_DetectPrivKeyFormat( + (byte*)pemOpenSshBody, (word32)(sizeof(pemOpenSshBody) - 1), + NULL, &keyDer, &privBuf, &privBufSz); + + Log(" Testing scenario: PEM decodes to an OpenSSH blob. %s\n", + (gotFormat == WOLFSSH_FORMAT_OPENSSH && keyDer != NULL) ? + "PASSED" : "FAILED"); + if (gotFormat != WOLFSSH_FORMAT_OPENSSH || keyDer == NULL) { + ret = WS_FATAL_ERROR; + } + + if (keyDer != NULL) { + WFREE(keyDer, NULL, DYNTYPE_SSHD); + } + if (ret != WS_SUCCESS) { + return ret; + } + } + return ret; } @@ -3999,6 +4233,9 @@ const TEST_CASE testCases[] = { #ifdef WOLFSSL_BASE64_ENCODE TEST_DECL(test_CheckAuthKeysLine), TEST_DECL(test_CheckAuthKeysLineTypes), + #ifndef WOLFSSH_NO_MLDSA + TEST_DECL(test_CheckAuthKeysLineMaxSz), + #endif #endif #if defined(WOLFSSL_BASE64_ENCODE) && !defined(_WIN32) TEST_DECL(test_SearchForPubKey), diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index e30148272..419e9f4f3 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -459,9 +459,8 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, WS_FORCEZERO(keyDer, dataSz); WFREE(keyDer, heap, DYNTYPE_SSHD); } - /* data held the raw private key — the DER bytes, or the PEM - * text decoded into keyDer above. Zeroize before freeing so key - * material does not linger in the heap after use. */ + /* data is the key material itself for raw DER/OpenSSH + * input (privBuf aliases it directly, no copy). */ WS_FORCEZERO(data, dataSz); freeBufferFromFile(data, heap); } diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index c1a485371..f05e59abb 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1851,66 +1851,88 @@ static int load_key_mldsa87(byte* buf, word32 bufSz) } #endif /* WOLFSSH_NO_MLDSA87 */ -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) -/* buf NULL: *bufSz set to file size; else file read into buf (bufSz max) */ -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 key buffer must be sized from the file, not a fixed constant */ static int LoadMlDsaCompositeHostKey(WOLFSSH_CTX* ctx, - int (*loadFn)(byte*, word32*), const char* label) + const char* fileName, const char* label) { +#ifndef NO_FILESYSTEM byte* compBuf = NULL; word32 compBufSz = 0; + word32 allocSz; word32 compSz; - loadFn(NULL, &compBufSz); + load_file(fileName, 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); + allocSz = compBufSz; + compBuf = (byte*)WMALLOC(allocSz, NULL, 0); if (compBuf == NULL) { fprintf(stderr, "Couldn't allocate %s key buffer.\n", label); return -1; } - compSz = loadFn(compBuf, &compBufSz); + compSz = load_file(fileName, compBuf, &compBufSz); if (compSz == 0) { + wc_ForceZero(compBuf, allocSz); WFREE(compBuf, NULL, 0); fprintf(stderr, "Couldn't load %s key file.\n", label); return -1; } if (wolfSSH_CTX_UsePrivateKey_buffer(ctx, compBuf, compSz, WOLFSSH_FORMAT_OPENSSH) < 0) { + wc_ForceZero(compBuf, allocSz); WFREE(compBuf, NULL, 0); fprintf(stderr, "Couldn't use %s key buffer.\n", label); return -1; } + wc_ForceZero(compBuf, allocSz); WFREE(compBuf, NULL, 0); return 0; +#else + (void)ctx; (void)fileName; + fprintf(stderr, "Couldn't load %s key: no filesystem.\n", label); + return -1; +#endif /* NO_FILESYSTEM */ } typedef struct { const char* substr; - int (*loadFn)(byte*, word32*); + const char* fileName; const char* label; } MlDsaCompositeEntry; /* NULL-terminated so the table is never empty if ECDSA and Ed25519/Ed448 * are both disabled */ static const MlDsaCompositeEntry mldsaCompositeEntries[] = { -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) - { "mldsa44-ed25519", load_key_mldsa44_ed25519, "ML-DSA-44+Ed25519" }, +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { "mldsa44-ed25519", "./keys/server-key-mldsa44ed25519", + "ML-DSA-44+Ed25519" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + { "mldsa44-es256", "./keys/server-key-mldsa44es256", + "ML-DSA-44+ES256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { "mldsa65-ed25519", "./keys/server-key-mldsa65ed25519", + "ML-DSA-65+Ed25519" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + { "mldsa65-es256", "./keys/server-key-mldsa65es256", + "ML-DSA-65+ES256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + { "mldsa87-ed448", "./keys/server-key-mldsa87ed448", + "ML-DSA-87+Ed448" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + { "mldsa87-es384", "./keys/server-key-mldsa87es384", + "ML-DSA-87+ES384" }, #endif { NULL, NULL, NULL } }; @@ -3328,6 +3350,69 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) { const char* bufName = NULL; int loadDefaultHostKeys = 1; + #ifndef WOLFSSH_NO_MLDSA + /* Defaults + requested composite keys can exceed + * WOLFSSH_MAX_PVT_KEYS; skip defaults if loading both would overflow. */ + if (keyList != NULL && WSTRSTR(keyList, "mldsa") != NULL) { + if (WSTRSTR(keyList, "rsa") == NULL && + WSTRSTR(keyList, "ecdsa") == NULL && + /* "ssh-ed25519" not "ed25519": composite combo names + * would false-positive on the bare substring. */ + WSTRSTR(keyList, "ssh-ed25519") == NULL) { + loadDefaultHostKeys = 0; + } + else { + int defaultKeyCount = 1; /* load_key(peerEcc, ...) */ + int compositeMatchCount = 0; + int plainMlDsaMatchCount = 0; + word32 idx; + + #if !defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_ECC) + defaultKeyCount++; /* load_key(!peerEcc, ...) */ + #endif + #ifndef WOLFSSH_NO_ED25519 + defaultKeyCount++; /* load_key_ed25519() */ + #endif + for (idx = 0; + mldsaCompositeEntries[idx].substr != NULL; idx++) { + if (WSTRSTR(keyList, + mldsaCompositeEntries[idx].substr) != NULL) { + compositeMatchCount++; + } + } + + /* Plain ssh-mldsa-NN keys are loaded below via + * LoadMlDsaHostKeys() and consume slots too. */ + #ifndef WOLFSSH_NO_MLDSA44 + if (WSTRSTR(keyList, "mldsa-44") != NULL) { + plainMlDsaMatchCount++; + } + #endif + #ifndef WOLFSSH_NO_MLDSA65 + if (WSTRSTR(keyList, "mldsa-65") != NULL) { + plainMlDsaMatchCount++; + } + #endif + #ifndef WOLFSSH_NO_MLDSA87 + if (WSTRSTR(keyList, "mldsa-87") != NULL) { + plainMlDsaMatchCount++; + } + #endif + + if (defaultKeyCount + compositeMatchCount + + plainMlDsaMatchCount > WOLFSSH_MAX_PVT_KEYS) { + fprintf(stderr, + "Default host keys (%d) + requested ML-DSA " + "keys (%d) would exceed WOLFSSH_MAX_PVT_KEYS " + "(%d); skipping default host keys.\n", + defaultKeyCount, + compositeMatchCount + plainMlDsaMatchCount, + WOLFSSH_MAX_PVT_KEYS); + loadDefaultHostKeys = 0; + } + } + } + #endif #ifndef WOLFSSH_SMALL_STACK byte buf[EXAMPLE_KEYLOAD_BUFFER_SZ]; #endif @@ -3457,7 +3542,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) if (WSTRSTR(keyList, entry->substr) != NULL) { mldsaMatched = 1; - if (LoadMlDsaCompositeHostKey(ctx, entry->loadFn, + if (LoadMlDsaCompositeHostKey(ctx, entry->fileName, entry->label) != 0) { #ifdef WOLFSSH_SMALL_STACK wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); diff --git a/keys/include.am b/keys/include.am index 82094705c..476678885 100644 --- a/keys/include.am +++ b/keys/include.am @@ -25,8 +25,12 @@ EXTRA_DIST+= \ keys/id_ecdsa keys/id_ecdsa.pub keys/id_rsa keys/id_rsa.pub \ keys/renewcerts.sh keys/renewcerts.cnf \ keys/server-key-ed25519.der keys/server-key-ed25519.pem \ - keys/server-key-mldsa44.der keys/server-key-mldsa65.der \ + keys/server-key-mldsa44.der keys/server-key-mldsa44.pem \ + keys/server-key-mldsa65.der \ keys/server-key-mldsa87.der keys/server-key-mldsa44ed25519 \ + keys/server-key-mldsa44es256 keys/server-key-mldsa65ed25519 \ + keys/server-key-mldsa65es256 keys/server-key-mldsa87ed448 \ + keys/server-key-mldsa87es384 \ keys/renew-ossh-certs.sh \ keys/ossh-ca keys/ossh-ca.pub \ keys/ossh-ca-rsa keys/ossh-ca-rsa.pub \ diff --git a/keys/server-key-mldsa44.pem b/keys/server-key-mldsa44.pem new file mode 100644 index 000000000..4ddbb8ff3 --- /dev/null +++ b/keys/server-key-mldsa44.pem @@ -0,0 +1,83 @@ +-----BEGIN PRIVATE KEY----- +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 +oexp1D0GP2IqStVF82Fvng==-----END PRIVATE KEY----- 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 1ad9c6971..784d7dc76 100644 --- a/src/internal.c +++ b/src/internal.c @@ -65,6 +65,20 @@ #ifndef WOLFSSH_NO_MLDSA #include + + /* SendKexGetSigningKey() bitwise-copies MlDsaKey, so it must have no + * heap-allocated/self-referential members; guard against that here. */ + #if defined(WOLFSSL_MLDSA_DYNAMIC_KEYS) || \ + (!defined(WC_MLDSA_FIXED_ARRAY) && \ + (defined(WC_MLDSA_CACHE_MATRIX_A) || \ + defined(WC_MLDSA_CACHE_PRIV_VECTORS) || \ + defined(WC_MLDSA_CACHE_PUB_VECTORS))) + #error "wolfSSH's ML-DSA composite key handling assumes MlDsaKey " \ + "is flat and safe to bitwise-copy; this wolfCrypt build " \ + "config gives it heap-allocated/pointer members, so " \ + "SendKexGetSigningKey() must be reworked before it can be " \ + "used with WOLFSSH_NO_MLDSA unset." + #endif #endif #ifdef NO_INLINE @@ -510,6 +524,9 @@ const char* GetErrorString(int err) case WS_MLDSA_E: return "ML-DSA error"; + case WS_ED448_E: + return "Ed448 buffer error"; + case WS_AUTH_PENDING: return "userauth is still pending (callback would block)"; @@ -525,7 +542,6 @@ const char* GetErrorString(int err) #endif } - static int wsHighwater(byte dir, void* ctx) { int ret = WS_SUCCESS; @@ -1017,9 +1033,28 @@ static const char cannedKexAlgoNames[] = /* ML-DSA listed first (post-quantum priority), then ECDSA, ED25519, RSA. */ static const char cannedKeyAlgoNames[] = -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + "ssh-mldsa87-ed448," +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + "ssh-mldsa87-es384," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + "ssh-mldsa65-ed25519," +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + "ssh-mldsa65-es256," +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) "ssh-mldsa44-ed25519@openssh.com," #endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + "ssh-mldsa44-es256," +#endif #ifndef WOLFSSH_NO_MLDSA87 "ssh-mldsa-87," #endif @@ -1773,11 +1808,16 @@ 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; + wc_MlDsaKey_Free(&key->ks.mldsa_composite.mldsa); 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); } @@ -2020,6 +2060,78 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, /* The OpenSSH binary key-format decoders (GetOpenSshKey, * IdentifyOpenSshKey, and their helpers) live in src/ossh.c. */ +/* + * Finds the OPENSSH PRIVATE KEY markers and base64-decodes between them. + * Shared by DoOpenSshKey() and wolfSSH_ProcessBuffer() so the two copies + * can't drift apart. + * + * @param in PEM buffer starting at the begin marker + * @param inSz size of in[] + * @param out receives the decoded bytes + * @param outSz in: capacity of out[]; out: decoded length + * @return WS_SUCCESS, or WS_PARSE_E if the markers or decode fail + */ +int WS_StripOpenSshPem(const byte* in, word32 inSz, byte* out, word32* outSz) +{ + static const char* beginMarker = "-----BEGIN OPENSSH PRIVATE KEY-----"; + static const char* endMarker = "-----END OPENSSH PRIVATE KEY-----"; + word32 beginSz = (word32)WSTRLEN(beginMarker); + word32 endSz = (word32)WSTRLEN(endMarker); + const char* footer; + const byte* b64; + word32 b64Sz; + + /* Reject buffers too small to hold both markers. Without this guard + * the subtraction used to locate the base64 region underflows inSz. */ + if (inSz <= beginSz + endSz) { + return WS_PARSE_E; + } + if (WMEMCMP(in, beginMarker, beginSz) != 0) { + return WS_PARSE_E; + } + footer = WSTRNSTR((const char*)in + beginSz, endMarker, inSz - beginSz); + if (footer == NULL) { + return WS_PARSE_E; + } + + b64 = in + beginSz; + b64Sz = (word32)(footer - (const char*)b64); + + return (Base64_Decode(b64, b64Sz, out, outSz) == 0) ? + WS_SUCCESS : WS_PARSE_E; +} + +#ifndef WOLFSSH_NO_MLDSA +/* Inits both halves of a composite key pair; mldsaInit/tradInit track + * which succeeded so the caller can clean up correctly on failure. */ +int InitCompositeKeyPair(const CompositeParams* params, + MlDsaKey* mldsa, void* tradKey, const CompositeTradOps* ops, + void* heap, int* mldsaInit, int* tradInit) +{ + int ret; + + *mldsaInit = 0; + *tradInit = 0; + + ret = wc_MlDsaKey_Init(mldsa, heap, INVALID_DEVID); + if (ret == 0) { + *mldsaInit = 1; + ret = wc_MlDsaKey_SetParams(mldsa, params->mldsaLevel); + } + if (ret == 0) { + if (ops == NULL) { + ret = WS_UNIMPLEMENTED_E; + } + else { + ret = ops->init(tradKey, heap); + if (ret == 0) *tradInit = 1; + } + } + + return ret; +} +#endif /* WOLFSSH_NO_MLDSA */ + #ifdef WOLFSSH_CERTS /* @@ -2512,37 +2624,12 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, der = (byte*)WMALLOC(inSz, heap, dynamicType); if (der == NULL) return WS_MEMORY_E; - /* strip PEM wrapper so IdentifyOpenSshKey sees raw binary; mirrors - * DoOpenSshKey() in src/ssh.c since wc_KeyPemToDer() doesn't know - * the OPENSSH PRIVATE KEY header */ + /* Strip the PEM wrapper so IdentifyOpenSshKey sees raw binary; + * mirrors DoOpenSshKey(), since wc_KeyPemToDer() doesn't know this. */ if (inSz >= 5 && WMEMCMP(in, "-----", 5) == 0) { - 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) { + if (WS_StripOpenSshPem(in, inSz, der, &derOutSz) != WS_SUCCESS) { WS_FORCEZERO(der, inSz); WFREE(der, heap, dynamicType); return WS_BAD_FILE_E; @@ -2607,12 +2694,15 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, return ret; } keyId = (byte)ret; - /* Only the composite key parsers know how to walk the stored - * openssh-key-v1 envelope later (SendKexGetSigningKey()); every - * other key type expects raw DER, so reject them here instead of - * silently storing an envelope that will fail every handshake. */ - if (format == WOLFSSH_FORMAT_OPENSSH && - keyId != ID_MLDSA44_ED25519) { + /* Only composite parsers can walk the stored openssh-key-v1 + * envelope; reject other key types now instead of at handshake. */ + if (format == WOLFSSH_FORMAT_OPENSSH +#ifndef WOLFSSH_NO_MLDSA + && keyId != ID_MLDSA44_ES256 && keyId != ID_MLDSA65_ES256 && + keyId != ID_MLDSA87_ES384 && keyId != ID_MLDSA44_ED25519 && + keyId != ID_MLDSA65_ED25519 && keyId != ID_MLDSA87_ED448 +#endif + ) { WS_FORCEZERO(der, derSz); WFREE(der, heap, dynamicType); return WS_UNIMPLEMENTED_E; @@ -2992,9 +3082,28 @@ static const NameIdPair NameIdMap[] = { #ifndef WOLFSSH_NO_MLDSA44 { ID_MLDSA44, TYPE_KEY, "ssh-mldsa-44" }, #endif -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + { ID_MLDSA44_ES256, TYPE_KEY, "ssh-mldsa44-es256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + { ID_MLDSA65_ES256, TYPE_KEY, "ssh-mldsa65-es256" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + { ID_MLDSA87_ES384, TYPE_KEY, "ssh-mldsa87-es384" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) { ID_MLDSA44_ED25519, TYPE_KEY, "ssh-mldsa44-ed25519@openssh.com" }, #endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { ID_MLDSA65_ED25519, TYPE_KEY, "ssh-mldsa65-ed25519" }, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + { ID_MLDSA87_ED448, TYPE_KEY, "ssh-mldsa87-ed448" }, +#endif #ifndef WOLFSSH_NO_MLDSA65 { ID_MLDSA65, TYPE_KEY, "ssh-mldsa-65" }, #endif @@ -4240,9 +4349,28 @@ static const byte cannedKeyAlgoClient[] = { #endif /* WOLFSSH_NO_SSH_RSA_SHA1 */ #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ #endif /* WOLFSSH_CERTS */ -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ID_MLDSA87_ED448, +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + ID_MLDSA87_ES384, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ID_MLDSA65_ED25519, +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + ID_MLDSA65_ES256, +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) ID_MLDSA44_ED25519, #endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + ID_MLDSA44_ES256, +#endif #ifndef WOLFSSH_NO_MLDSA87 ID_MLDSA87, #endif @@ -4761,14 +4889,17 @@ static int IsKexMatchError(int ret) ret == WS_MATCH_ENC_ALGO_E || ret == WS_MATCH_MAC_ALGO_E; } +/* Headroom for DoKexInit()'s decoded name-lists; bump if a future + * algorithm addition pushes a canned list close to this limit. */ +#define WOLFSSH_KEXINIT_ID_LIST_MAX 32 static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) { int ret = WS_SUCCESS; int side = WOLFSSH_ENDPOINT_SERVER; byte algoId; - byte list[24] = {ID_NONE}; - byte cannedList[24] = {ID_NONE}; + byte list[WOLFSSH_KEXINIT_ID_LIST_MAX] = {ID_NONE}; + byte cannedList[WOLFSSH_KEXINIT_ID_LIST_MAX] = {ID_NONE}; byte kexIdGuess = ID_NONE; byte pubKeyIdGuess = ID_NONE; byte kexPacketFollows = 0; @@ -5464,24 +5595,7 @@ struct wolfSSH_sigKeyBlock { #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) - /* keep union non-empty (empty union rejected by some - * compilers) though unusable without a trad component */ - byte placeholder; -#endif - } trad; + WS_MlDsaCompositeBody base; /* largest mldsaPubSz + tradPubSz across WS_GetCompositeParams() * combos; keep in sync with any new combo added there */ byte q[WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; @@ -6091,12 +6205,16 @@ static int ParsePubKey(WOLFSSH *ssh, pubKeySz, ssh->handshake->pubKeyId); break; #endif -#endif -#ifndef WOLFSSH_NO_MLDSA + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: sigKeyBlock_ptr->useMlDsaComposite = 1; sigKeyBlock_ptr->pubKeyId = ssh->handshake->pubKeyId; - ret = ParseMlDsaCompositePubKey(ssh, sigKeyBlock_ptr, pubKey, pubKeySz, ssh->handshake->pubKeyId); + ret = ParseMlDsaCompositePubKey(ssh, sigKeyBlock_ptr, pubKey, + pubKeySz, ssh->handshake->pubKeyId); break; #endif @@ -6168,11 +6286,11 @@ static void FreePubKey(struct wolfSSH_sigKeyBlock *p) #ifndef WOLFSSH_NO_MLDSA else if (p->useMlDsaComposite) { CompositeParams params; + wc_MlDsaKey_Free(&p->sk.mldsa_composite.base.mldsa); if (WS_GetCompositeParams(p->pubKeyId, ¶ms) == WS_SUCCESS) { const CompositeTradOps* ops = WS_GetTradOps(params.tradType); - wc_MlDsaKey_Free(&p->sk.mldsa_composite.mldsa); if (ops != NULL) { - ops->free(&p->sk.mldsa_composite.trad); + ops->free(&p->sk.mldsa_composite.base.trad); } } } @@ -7046,13 +7164,13 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) 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, + &sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa, + &sigKeyBlock_ptr->sk.mldsa_composite.base.trad, sig, sigSz, ssh->h, ssh->hSz); if (ret != WS_SUCCESS) { WLOG(WS_LOG_DEBUG, - "DoKexDhReply: ML-DSA Composite Signature Verify fail (%d)", - ret); + "DoKexDhReply: ML-DSA Composite Signature " + "Verify fail (%d)", ret); } } #endif @@ -9614,11 +9732,15 @@ 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); + else if (pkTypeId == ID_MLDSA44_ES256 || + pkTypeId == ID_MLDSA65_ES256 || + pkTypeId == ID_MLDSA87_ES384 || + pkTypeId == ID_MLDSA44_ED25519 || + pkTypeId == ID_MLDSA65_ED25519 || + pkTypeId == ID_MLDSA87_ED448) { + ret = DoUserAuthRequestMlDsaComposite(ssh, + &authData->sf.publicKey, + authData, pkTypeId, pubKeyBlobSz); } #endif else { @@ -12946,27 +13068,9 @@ 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) - /* keep union non-empty (empty union rejected by some - * compilers) though unusable without a trad component */ - byte placeholder; -#endif - } trad; + WS_MlDsaCompositeBody base; + byte tradInit; /* largest mldsaPubSz + tradPubSz across * WS_GetCompositeParams() combos; keep in sync */ byte q[WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ]; @@ -13624,7 +13728,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); @@ -13634,10 +13743,8 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, 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). */ + /* privateKey[keyIdx].key is the raw envelope, not just + * key data; must go through GetOpenSshKey() to walk it. */ { WS_KeySignature keySig; word32 idx = 0; @@ -13649,50 +13756,106 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, ret = GetOpenSshKey(&keySig, ssh->ctx->privateKey[keyIdx].key, ssh->ctx->privateKey[keyIdx].keySz, &idx); - if (ret != WS_SUCCESS || keySig.keyId != sigKeyBlock_ptr->pubKeyId) { + 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) { - /* shallow copy: ownership moves, don't reuse keySig */ - sigKeyBlock_ptr->sk.mldsa_composite.mldsa = + /* ecc_key is self-referential under ALT_ECC_SIZE, so + * rebuild via export/import; others copy flat safely. */ + sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa = keySig.ks.mldsa_composite.mldsa; - XMEMCPY(&sigKeyBlock_ptr->sk.mldsa_composite.trad, - &keySig.ks.mldsa_composite.trad, - sizeof(sigKeyBlock_ptr->sk.mldsa_composite.trad)); + wc_MlDsaKey_Free(&keySig.ks.mldsa_composite.mldsa); +#ifndef WOLFSSH_NO_ECDSA + if (params.tradType == TRAD_TYPE_ECC) { + byte eccPriv[MAX_ECC_BYTES]; + byte eccPub[MAX_ECC_BYTES * 2 + 1]; + word32 eccPrivSz = params.tradPrivSz; + word32 eccPubSz = params.tradPubSz; + + ret = wc_ecc_export_private_only( + &keySig.ks.mldsa_composite.trad.ecc, + eccPriv, &eccPrivSz); + if (ret == 0) { + ret = wc_ecc_export_x963( + &keySig.ks.mldsa_composite.trad.ecc, + eccPub, &eccPubSz); + } + if (ret == 0) { + ret = wc_ecc_init_ex(&sigKeyBlock_ptr-> + sk.mldsa_composite.base.trad.ecc, + heap, INVALID_DEVID); + if (ret == 0) { + sigKeyBlock_ptr->sk.mldsa_composite.tradInit = 1; + } + } + if (ret == 0) { + ret = wc_ecc_import_private_key(eccPriv, + eccPrivSz, eccPub, eccPubSz, + &sigKeyBlock_ptr-> + sk.mldsa_composite.base.trad.ecc); + } + wc_ecc_free(&keySig.ks.mldsa_composite.trad.ecc); + wc_ForceZero(eccPriv, sizeof(eccPriv)); + /* caller's cleanup frees base.mldsa/.trad on + * failure too; don't free here. */ + if (ret != 0) { + ret = WS_CRYPTO_FAILED; + } + } + else +#endif + { + const CompositeTradOps* tradOps = + WS_GetTradOps(params.tradType); + WS_MlDsaCompositeBody* base = + &sigKeyBlock_ptr->sk.mldsa_composite.base; + XMEMCPY(&base->trad, + &keySig.ks.mldsa_composite.trad, + sizeof(base->trad)); + sigKeyBlock_ptr->sk.mldsa_composite.tradInit = 1; + if (tradOps != NULL) { + tradOps->free(&keySig.ks.mldsa_composite.trad); + } + } } } if (ret == 0) { word32 mldsaPubSz = params.mldsaPubSz; ret = wc_MlDsaKey_ExportPubRaw( - &sigKeyBlock_ptr->sk.mldsa_composite.mldsa, + &sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa, sigKeyBlock_ptr->sk.mldsa_composite.q, &mldsaPubSz); if (ret == 0) { - const CompositeTradOps* ops = WS_GetTradOps(params.tradType); + 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, + &sigKeyBlock_ptr-> + sk.mldsa_composite.base.trad, + sigKeyBlock_ptr->sk.mldsa_composite.q + + params.mldsaPubSz, &eccPubSz); } } if (ret == 0) { - sigKeyBlock_ptr->sk.mldsa_composite.qSz = params.mldsaPubSz + params.tradPubSz; + 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; + sigKeyBlock_ptr->pubKeyFmtNameSz + + sigKeyBlock_ptr->sk.mldsa_composite.qSz; c32toa(sigKeyBlock_ptr->sz, scratchLen); ret = wc_HashUpdate(hash, hashId, scratchLen, LENGTH_SZ); @@ -13704,17 +13867,18 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, } if (ret == 0) ret = wc_HashUpdate(hash, hashId, - (byte*)sigKeyBlock_ptr->pubKeyFmtName, - sigKeyBlock_ptr->pubKeyFmtNameSz); + (byte*)sigKeyBlock_ptr->pubKeyFmtName, + sigKeyBlock_ptr->pubKeyFmtNameSz); if (ret == 0) { - c32toa(sigKeyBlock_ptr->sk.mldsa_composite.qSz, scratchLen); + 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); + sigKeyBlock_ptr->sk.mldsa_composite.q, + sigKeyBlock_ptr->sk.mldsa_composite.qSz); } } break; @@ -14870,9 +15034,12 @@ 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_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 @@ -15207,14 +15374,20 @@ int SendKexDhReply(WOLFSSH* ssh) ) { wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa.key); } - else if (sigKeyBlock_ptr->pubKeyId == ID_MLDSA44_ED25519) { + else if (sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA44_ES256 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA65_ES256 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA87_ES384 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA44_ED25519 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA65_ED25519 + || sigKeyBlock_ptr->pubKeyFmtId == ID_MLDSA87_ED448) { CompositeParams params; - if (WS_GetCompositeParams(sigKeyBlock_ptr->pubKeyId, ¶ms) + wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa); + if (sigKeyBlock_ptr->sk.mldsa_composite.tradInit && + WS_GetCompositeParams(sigKeyBlock_ptr->pubKeyFmtId, ¶ms) == WS_SUCCESS) { const CompositeTradOps* ops = WS_GetTradOps(params.tradType); - wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.mldsa); if (ops != NULL) { - ops->free(&sigKeyBlock_ptr->sk.mldsa_composite.trad); + ops->free(&sigKeyBlock_ptr->sk.mldsa_composite.base.trad); } } } @@ -15358,7 +15531,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: { /* composite key block: ML-DSA pubkey followed by trad pubkey */ c32toa(sigKeyBlock_ptr->sk.mldsa_composite.qSz, output + idx); @@ -18041,7 +18219,7 @@ static int BuildUserAuthRequestMlDsa(WOLFSSH* ssh, return ret; } - sigSz = (word32)keySig->sigSz; + sigSz = keySig->sigSz; sig = (byte*)WMALLOC(sigSz, keySig->heap, DYNTYPE_BUFFER); if (sig == NULL) @@ -18259,7 +18437,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; @@ -18438,7 +18621,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, @@ -18448,8 +18636,8 @@ static int BuildUserAuthRequestPublicKey(WOLFSSH* ssh, begin += LENGTH_SZ; WMEMCPY(output + begin, pk->publicKey, pk->publicKeySz); begin += pk->publicKeySz; - ret = BuildUserAuthRequestMlDsaComposite(ssh, output, &begin, - authData, sigStart, sigStartIdx, keySig); + ret = BuildUserAuthRequestMlDsaComposite(ssh, output, + &begin, authData, sigStart, sigStartIdx, keySig); break; #endif default: @@ -20748,7 +20936,61 @@ int WS_GetCompositeParams(byte keyId, CompositeParams* params) params->keyId = keyId; switch (keyId) { -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + case ID_MLDSA44_ES256: + params->mldsaLevel = WC_ML_DSA_44; + params->mldsaSigSz = WC_MLDSA_44_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_44_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ECC; + params->tradHashId = WC_HASH_TYPE_SHA256; + params->tradHashSz = WC_SHA256_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA44-ECDSA-P256-SHA256"; + params->labelSz = (word32)XSTRLEN(params->label); + /* uncompressed point: 1 (type octet) + 2 * coordinate */ + params->tradPubSz = 1 + (2 * ECC_P256_COORD_SZ); + /* worst case: 2 * (LENGTH_SZ + P256 coordinate + sign pad) */ + params->tradSigSz = 2 * (LENGTH_SZ + ECC_P256_COORD_SZ + 1); + params->tradPrivSz = ECC_P256_COORD_SZ; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + case ID_MLDSA65_ES256: + params->mldsaLevel = WC_ML_DSA_65; + params->mldsaSigSz = WC_MLDSA_65_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_65_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ECC; + params->tradHashId = WC_HASH_TYPE_SHA512; + params->tradHashSz = WC_SHA512_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA65-ECDSA-P256-SHA512"; + params->labelSz = (word32)XSTRLEN(params->label); + /* uncompressed point: 1 (type octet) + 2 * coordinate */ + params->tradPubSz = 1 + (2 * ECC_P256_COORD_SZ); + /* worst case: 2 * (LENGTH_SZ + P256 coordinate + sign pad) */ + params->tradSigSz = 2 * (LENGTH_SZ + ECC_P256_COORD_SZ + 1); + params->tradPrivSz = ECC_P256_COORD_SZ; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + case ID_MLDSA87_ES384: + params->mldsaLevel = WC_ML_DSA_87; + params->mldsaSigSz = WC_MLDSA_87_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_87_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ECC; + params->tradHashId = WC_HASH_TYPE_SHA512; + params->tradHashSz = WC_SHA512_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA87-ECDSA-P384-SHA512"; + params->labelSz = (word32)XSTRLEN(params->label); + /* uncompressed point: 1 (type octet) + 2 * coordinate */ + params->tradPubSz = 1 + (2 * ECC_P384_COORD_SZ); + /* worst case: 2 * (LENGTH_SZ + P384 coordinate + sign pad) */ + params->tradSigSz = 2 * (LENGTH_SZ + ECC_P384_COORD_SZ + 1); + params->tradPrivSz = ECC_P384_COORD_SZ; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) case ID_MLDSA44_ED25519: params->mldsaLevel = WC_ML_DSA_44; params->mldsaSigSz = WC_MLDSA_44_SIG_SIZE; @@ -20762,6 +21004,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) && \ + !defined(NO_SHA512) + case ID_MLDSA65_ED25519: + params->mldsaLevel = WC_ML_DSA_65; + params->mldsaSigSz = WC_MLDSA_65_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_65_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ED25519; + params->tradHashId = WC_HASH_TYPE_SHA512; + params->tradHashSz = WC_SHA512_DIGEST_SIZE; + params->label = "COMPSIG-MLDSA65-Ed25519-SHA512"; + params->labelSz = (word32)XSTRLEN(params->label); + params->tradPubSz = ED25519_PUB_KEY_SIZE; + params->tradSigSz = ED25519_SIG_SIZE; + params->tradPrivSz = ED25519_KEY_SIZE; + break; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + case ID_MLDSA87_ED448: + params->mldsaLevel = WC_ML_DSA_87; + params->mldsaSigSz = WC_MLDSA_87_SIG_SIZE; + params->mldsaPubSz = WC_MLDSA_87_PUB_KEY_SIZE; + params->tradType = TRAD_TYPE_ED448; + params->tradHashId = WC_HASH_TYPE_SHAKE256; + /* SHAKE256 truncated to a fixed 64-byte digest for this combo */ + params->tradHashSz = 64; + params->label = "COMPSIG-MLDSA87-Ed448-SHAKE256"; + params->labelSz = (word32)XSTRLEN(params->label); + params->tradPubSz = ED448_PUB_KEY_SIZE; + params->tradSigSz = ED448_SIG_SIZE; + params->tradPrivSz = ED448_KEY_SIZE; + break; #endif default: return WS_BAD_ARGUMENT; @@ -20779,7 +21053,8 @@ int WS_GetCompositeParams(byte keyId, CompositeParams* params) return WS_SUCCESS; } -int WS_Hash_Helper(enum wc_HashType hashId, const byte* msg, word32 msgSz, byte* hash, word32 hashSz) +int WS_Hash_Helper(enum wc_HashType hashId, const byte* msg, word32 msgSz, + byte* hash, word32 hashSz) { int ret; #ifdef WOLFSSL_SHAKE256 @@ -20791,6 +21066,7 @@ int WS_Hash_Helper(enum wc_HashType hashId, const byte* msg, word32 msgSz, byte* if (ret == 0) { ret = wc_Shake256_Final(&shake, hash, hashSz); } + wc_Shake256_Free(&shake); } return ret; } @@ -20926,7 +21202,7 @@ static int CompositeEccSign(void* key, WC_RNG* rng, void* heap, } #endif } - if (ret != 0 && ret != WS_BAD_ARGUMENT) { + if (ret != 0 && ret != WS_BAD_ARGUMENT && ret != WS_MEMORY_E) { ret = WS_ECC_E; } @@ -20972,6 +21248,11 @@ static int CompositeEccVerify(void* key, void* heap, if (ret == WS_SUCCESS) { ret = GetStringRef(&sSz, &s, wireSig, wireSigSz, &i); } + /* GetStringRef() only bounds-checks each string; it doesn't require + * reaching the end, so reject any trailing bytes after r/s here. */ + if (ret == WS_SUCCESS && i != wireSigSz) { + ret = WS_KEY_FORMAT_E; + } if (ret == WS_SUCCESS) { ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz, asnSig, &asnSigSz); if (ret != 0) ret = WS_ECC_E; @@ -21107,6 +21388,101 @@ static const CompositeTradOps compositeEd25519Ops = { }; #endif /* !WOLFSSH_NO_ED25519 */ +#ifdef HAVE_ED448 +/* returns 0 on success, negative wc_ed448_init_ex() code on failure */ +static int CompositeEd448Init(void* key, void* heap) +{ + return wc_ed448_init_ex((ed448_key*)key, heap, INVALID_DEVID); +} + +/* no return value */ +static void CompositeEd448Free(void* key) +{ + wc_ed448_free((ed448_key*)key); +} + +/* returns 0 on success, negative wc_ed448_import_public() code on failure */ +static int CompositeEd448ImportPub(void* key, const byte* pub, word32 pubSz) +{ + return wc_ed448_import_public(pub, pubSz, (ed448_key*)key); +} + +/* returns 0 on success, negative wc_ed448_import_private_key() code on + * failure */ +static int CompositeEd448ImportPriv(void* key, const byte* priv, + word32 privSz, const byte* pub, word32 pubSz) +{ + return wc_ed448_import_private_key(priv, privSz, pub, pubSz, + (ed448_key*)key); +} + +/* returns 0 on success, negative wc_ed448_export_public() code on failure */ +static int CompositeEd448ExportPub(void* key, byte* out, word32* outSz) +{ + return wc_ed448_export_public((ed448_key*)key, out, outSz); +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEd448Sign(void* key, WC_RNG* rng, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* mPrime, word32 mPrimeLen, + byte* wireSig, word32* wireSigSz) +{ + int ret; + word32 sigSz = ED448_SIG_SIZE; + + (void)rng; + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + if (*wireSigSz < ED448_SIG_SIZE) { + return WS_BAD_ARGUMENT; + } + + ret = wc_ed448_sign_msg(mPrime, mPrimeLen, wireSig, &sigSz, + (ed448_key*)key, NULL, 0); + if (ret != 0 || sigSz != ED448_SIG_SIZE) { + ret = WS_ED448_E; + } + else { + *wireSigSz = sigSz; + } + + return ret; +} + +/* returns WS_SUCCESS on success, negative WS_* error code on failure */ +static int CompositeEd448Verify(void* key, void* heap, + enum wc_HashType tradHashId, word32 tradHashSz, + const byte* wireSig, word32 wireSigSz, + const byte* mPrime, word32 mPrimeLen) +{ + int ret; + int res = 0; + + (void)heap; + (void)tradHashId; + (void)tradHashSz; + + ret = wc_ed448_verify_msg(wireSig, wireSigSz, mPrime, mPrimeLen, + &res, (ed448_key*)key, NULL, 0); + if (ret != 0 || res != 1) { + ret = WS_ED448_E; + } + + return ret; +} + +static const CompositeTradOps compositeEd448Ops = { + CompositeEd448Init, CompositeEd448Free, + CompositeEd448ImportPub, CompositeEd448ImportPriv, + CompositeEd448ExportPub, + CompositeEd448Sign, CompositeEd448Verify, + TRAD_TYPE_ED448 +}; +#endif /* HAVE_ED448 */ + /* returns matching CompositeTradOps for tradType, NULL if unsupported */ const CompositeTradOps* WS_GetTradOps(byte tradType) { @@ -21118,6 +21494,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; @@ -21129,6 +21509,19 @@ const CompositeTradOps* WS_GetTradOps(byte tradType) (COMPOSITE_DOMAIN_PREFIX_SZ + COMPOSITE_MAX_LABEL_SZ + 1 + \ WC_MAX_DIGEST_SIZE) +/* Assembles mPrime = PREFIX||label||0x00||hash; single source of truth + * for the domain-separator layout so sign/verify can't drift apart. */ +static void BuildCompositeMPrime(const CompositeParams* params, + const byte* hash, byte* mPrime) +{ + XMEMCPY(mPrime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ, params->label, + params->labelSz); + mPrime[COMPOSITE_DOMAIN_PREFIX_SZ + params->labelSz] = 0; + XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ + params->labelSz + 1, hash, + params->tradHashSz); +} + /* returns WS_SUCCESS if sig verifies, negative WS_* error code otherwise */ static int VerifyMlDsaComposite(byte keyId, void* heap, MlDsaKey* mldsa, void* tradKey, @@ -21160,13 +21553,17 @@ static int VerifyMlDsaComposite(byte keyId, void* heap, return WS_KEY_FORMAT_E; } - /* ED25519/ED448 are fixed size; ECC's variable r/s is checked via - * GetStringRef() below instead */ - if (params.tradType == TRAD_TYPE_ED25519 || params.tradType == TRAD_TYPE_ED448) { + /* ED25519/ED448 are fixed size and must match exactly; ECC's r/s + * are variable, so bound the trad region too against oversized claims. */ + if (params.tradType == TRAD_TYPE_ED25519 || + params.tradType == TRAD_TYPE_ED448) { if (sigSz != (params.mldsaSigSz + params.tradSigSz)) { return WS_KEY_FORMAT_E; } } + else if (sigSz - params.mldsaSigSz > params.tradSigSz) { + return WS_KEY_FORMAT_E; + } #ifdef WOLFSSH_SMALL_STACK hash = (byte*)WMALLOC(WC_MAX_DIGEST_SIZE, heap, DYNTYPE_TEMP); @@ -21179,18 +21576,17 @@ static int VerifyMlDsaComposite(byte keyId, void* heap, } #endif - mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + params.tradHashSz; + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + + params.tradHashSz; if (ret == WS_SUCCESS) { - ret = WS_Hash_Helper(params.tradHashId, msg, msgSz, hash, params.tradHashSz); + ret = WS_Hash_Helper(params.tradHashId, msg, msgSz, hash, + params.tradHashSz); if (ret != 0) ret = WS_CRYPTO_FAILED; } if (ret == WS_SUCCESS) { - XMEMCPY(mPrime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); - XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); - mPrime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; - XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + BuildCompositeMPrime(¶ms, hash, mPrime); } /* cheap trad verify first, so garbage sigs are rejected cheaply @@ -21213,11 +21609,13 @@ static int VerifyMlDsaComposite(byte keyId, void* heap, if (ret == WS_SUCCESS) { ret = wc_MlDsaKey_VerifyCtx(mldsa, sig, params.mldsaSigSz, - (const byte*)params.label, (byte)params.labelSz, + (const byte*)params.label, params.labelSz, mPrime, mPrimeLen, &status); if (ret != 0 || status != 1) { - WLOG(WS_LOG_DEBUG, "VerifyMlDsaComposite: ML-DSA Verify fail (%d, status=%d)", ret, status); + WLOG(WS_LOG_DEBUG, + "VerifyMlDsaComposite: ML-DSA Verify fail (%d, status=%d)", + ret, status); ret = WS_MLDSA_E; } } @@ -21245,6 +21643,8 @@ static int ParseMlDsaCompositePubKey(WOLFSSH* ssh, byte* pubKey, word32 pubKeySz, byte keyId) { int ret; + int mldsaInit = 0; + int tradInit = 0; const byte* pub; word32 pubSz, pubKeyIdx = 0; CompositeParams params; @@ -21255,32 +21655,14 @@ static int ParseMlDsaCompositePubKey(WOLFSSH* ssh, 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; - } + ret = InitCompositeKeyPair(¶ms, + &sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa, + &sigKeyBlock_ptr->sk.mldsa_composite.base.trad, ops, + ssh->ctx->heap, &mldsaInit, &tradInit); - 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; + if (ret == WS_SUCCESS) { + ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx); } - - ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx); if (ret == WS_SUCCESS) ret = GetStringRef(&pubSz, &pub, pubKey, pubKeySz, &pubKeyIdx); if (ret == WS_SUCCESS) { @@ -21289,20 +21671,30 @@ static int ParseMlDsaCompositePubKey(WOLFSSH* ssh, } } if (ret == WS_SUCCESS) - ret = wc_MlDsaKey_ImportPubRaw(&sigKeyBlock_ptr->sk.mldsa_composite.mldsa, + ret = wc_MlDsaKey_ImportPubRaw( + &sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa, pub, params.mldsaPubSz); if (ret == WS_SUCCESS) { - ret = ops->importPub(&sigKeyBlock_ptr->sk.mldsa_composite.trad, + ret = ops->importPub(&sigKeyBlock_ptr->sk.mldsa_composite.base.trad, pub + params.mldsaPubSz, params.tradPubSz); } - if (ret == 0) { + if (ret == WS_SUCCESS) { 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; + if (mldsaInit) { + wc_MlDsaKey_Free(&sigKeyBlock_ptr->sk.mldsa_composite.base.mldsa); + } + if (tradInit) { + ops->free(&sigKeyBlock_ptr->sk.mldsa_composite.base.trad); + } + /* preserve diagnosable reasons; collapse raw wolfCrypt codes from + * ImportPubRaw/importPub to keep a stable public error surface */ + if (ret != WS_UNIMPLEMENTED_E && ret != WS_MEMORY_E && + ret != WS_KEY_FORMAT_E) { + ret = WS_INVALID_ALGO_ID; + } } return ret; } @@ -21352,27 +21744,28 @@ static int SignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, } #endif - mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + params.tradHashSz; + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + + params.tradHashSz; if (ret == WS_SUCCESS) { - ret = WS_Hash_Helper(params.tradHashId, ssh->h, ssh->hSz, hash, params.tradHashSz); + 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(mPrime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); - XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); - mPrime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; - XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + BuildCompositeMPrime(¶ms, hash, mPrime); } if (ret == WS_SUCCESS) { - ret = wc_MlDsaKey_SignCtx(&sigKey->sk.mldsa_composite.mldsa, + ret = wc_MlDsaKey_SignCtx(&sigKey->sk.mldsa_composite.base.mldsa, (const byte*)params.label, params.labelSz, - sig, &mldsaSigSz, mPrime, mPrimeLen, ssh->rng); + sig, &mldsaSigSz, mPrime, mPrimeLen, + ssh->rng); if (ret != 0 || mldsaSigSz != params.mldsaSigSz) { - WLOG(WS_LOG_DEBUG, "SignHMlDsaComposite: ML-DSA sign fail (%d)", ret); + WLOG(WS_LOG_DEBUG, "SignHMlDsaComposite: ML-DSA sign fail (%d)", + ret); ret = WS_MLDSA_E; } } @@ -21384,14 +21777,15 @@ static int SignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, } else { word32 wireSigSz = params.tradSigSz; - ret = ops->sign(&sigKey->sk.mldsa_composite.trad, ssh->rng, + ret = ops->sign(&sigKey->sk.mldsa_composite.base.trad, ssh->rng, ssh->ctx->heap, params.tradHashId, params.tradHashSz, mPrime, mPrimeLen, sig + params.mldsaSigSz, &wireSigSz); if (ret == WS_SUCCESS) { *sigSz = params.mldsaSigSz + wireSigSz; } else { - WLOG(WS_LOG_DEBUG, "SignHMlDsaComposite: trad sign fail (%d)", ret); + WLOG(WS_LOG_DEBUG, "SignHMlDsaComposite: trad sign fail (%d)", + ret); } } } @@ -21433,8 +21827,8 @@ static int PrepareUserAuthRequestMlDsaComposite(WOLFSSH* ssh, word32* payloadSz, if (ret == WS_SUCCESS) { word32 idx = 0; - /* composite keys are OpenSSH-format only; GetOpenSshKey() also - * self-initializes keySig, so no pre-init needed */ + /* Composite keys are OpenSSH-format only; keySig is already + * zeroed/keyId-set by the caller, no further pre-init needed. */ ret = GetOpenSshKey(keySig, authData->sf.publicKey.privateKey, authData->sf.publicKey.privateKeySz, &idx); @@ -21448,12 +21842,14 @@ static int PrepareUserAuthRequestMlDsaComposite(WOLFSSH* ssh, word32* payloadSz, if (ret == WS_SUCCESS) { if (authData->sf.publicKey.hasSignature) { word32 sigSz = params.mldsaSigSz + params.tradSigSz; - *payloadSz += (LENGTH_SZ * 3) + sigSz + authData->sf.publicKey.publicKeyTypeSz; + *payloadSz += ( + LENGTH_SZ * 3) + sigSz + authData->sf.publicKey.publicKeyTypeSz; keySig->sigSz = sigSz; } } - WLOG(WS_LOG_DEBUG, "Leaving PrepareUserAuthRequestMlDsaComposite(), ret = %d", ret); + WLOG(WS_LOG_DEBUG, + "Leaving PrepareUserAuthRequestMlDsaComposite(), ret = %d", ret); return ret; } @@ -21487,7 +21883,7 @@ static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, if (ret != WS_SUCCESS) return ret; mldsaSigSz = params.mldsaSigSz; - sigSz = (word32)keySig->sigSz; + sigSz = keySig->sigSz; /* sigSz is already the worst-case total; slack below isn't load-bearing */ sig = (byte*)WMALLOC(sigSz + COMPOSITE_SIG_ALLOC_SLACK_SZ, @@ -21516,7 +21912,8 @@ static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, if (ret == WS_SUCCESS) { hash = (byte*)WMALLOC(params.tradHashSz, keySig->heap, DYNTYPE_TEMP); - mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1 + params.tradHashSz; + mPrimeLen = COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + + 1 + params.tradHashSz; mPrime = (byte*)WMALLOC(mPrimeLen, keySig->heap, DYNTYPE_TEMP); if (hash == NULL || mPrime == NULL) { @@ -21525,17 +21922,15 @@ static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, } if (ret == WS_SUCCESS) { - ret = WS_Hash_Helper(params.tradHashId, checkData, checkDataSz, hash, params.tradHashSz); + ret = WS_Hash_Helper(params.tradHashId, checkData, checkDataSz, hash, + params.tradHashSz); if (ret != 0) { ret = WS_CRYPTO_FAILED; } } if (ret == WS_SUCCESS) { - XMEMCPY(mPrime, COMPOSITE_DOMAIN_PREFIX, COMPOSITE_DOMAIN_PREFIX_SZ); - XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ, params.label, params.labelSz); - mPrime[COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz] = 0; - XMEMCPY(mPrime + COMPOSITE_DOMAIN_PREFIX_SZ + params.labelSz + 1, hash, params.tradHashSz); + BuildCompositeMPrime(¶ms, hash, mPrime); } if (ret == WS_SUCCESS) { @@ -21543,9 +21938,11 @@ static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, ret = wc_MlDsaKey_SignCtx(&keySig->ks.mldsa_composite.mldsa, (const byte*)params.label, params.labelSz, - sig, &mldsaSigSz, mPrime, mPrimeLen, ssh->rng); + sig, &mldsaSigSz, mPrime, mPrimeLen, + ssh->rng); if (ret != 0 || mldsaSigSz != params.mldsaSigSz) { - WLOG(WS_LOG_DEBUG, "BUARMlDsaComposite: ML-DSA sign fail (%d)", ret); + WLOG(WS_LOG_DEBUG, + "BUARMlDsaComposite: ML-DSA sign fail (%d)", ret); ret = WS_MLDSA_E; } } @@ -21557,7 +21954,8 @@ static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, } else { word32 wireSigSz = params.tradSigSz; - WLOG(WS_LOG_INFO, "Signing with hybrid composite (trad component)."); + WLOG(WS_LOG_INFO, + "Signing with hybrid composite (trad component)."); ret = ops->sign(&keySig->ks.mldsa_composite.trad, ssh->rng, keySig->heap, params.tradHashId, params.tradHashSz, mPrime, mPrimeLen, sig + params.mldsaSigSz, &wireSigSz); @@ -21565,7 +21963,8 @@ static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, sigSz = params.mldsaSigSz + wireSigSz; } else { - WLOG(WS_LOG_DEBUG, "BUARMlDsaComposite: trad sign fail (%d)", ret); + WLOG(WS_LOG_DEBUG, "BUARMlDsaComposite: trad sign fail (%d)", + ret); } } } @@ -21577,7 +21976,8 @@ static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, c32toa(authData->sf.publicKey.publicKeyTypeSz, output + begin); begin += LENGTH_SZ; - WMEMCPY(output + begin, authData->sf.publicKey.publicKeyType, authData->sf.publicKey.publicKeyTypeSz); + WMEMCPY(output + begin, authData->sf.publicKey.publicKeyType, + authData->sf.publicKey.publicKeyTypeSz); begin += authData->sf.publicKey.publicKeyTypeSz; c32toa(sigSz, output + begin); @@ -21602,11 +22002,12 @@ static int BuildUserAuthRequestMlDsaComposite(WOLFSSH* ssh, WFREE(hash, keySig->heap, DYNTYPE_TEMP); } if (mPrime != NULL) { - wc_ForceZero(mPrime, mPrimeLen); + WS_FORCEZERO(mPrime, mPrimeLen); WFREE(mPrime, keySig->heap, DYNTYPE_TEMP); } - WLOG(WS_LOG_DEBUG, "Leaving BuildUserAuthRequestMlDsaComposite(), ret = %d", ret); + WLOG(WS_LOG_DEBUG, + "Leaving BuildUserAuthRequestMlDsaComposite(), ret = %d", ret); return ret; } @@ -21632,7 +22033,8 @@ static int DoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, ret = WS_GetCompositeParams(keyId, ¶ms); if (ret != WS_SUCCESS) return ret; - keySig = (WS_KeySignature*)WMALLOC(sizeof(WS_KeySignature), ssh->ctx->heap, DYNTYPE_PUBKEY); + keySig = (WS_KeySignature*)WMALLOC(sizeof(WS_KeySignature), ssh->ctx->heap, + DYNTYPE_PUBKEY); if (keySig == NULL) { ret = WS_MEMORY_E; } @@ -21647,20 +22049,9 @@ static int DoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, 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; - } - } + ret = InitCompositeKeyPair(¶ms, &keySig->ks.mldsa_composite.mldsa, + &keySig->ks.mldsa_composite.trad, ops, keySig->heap, + &mldsaInit, &tradInit); if (ret == 0) { ret = GetSize(&publicKeyTypeSz, pk->publicKey, pk->publicKeySz, &i); @@ -21676,14 +22067,17 @@ static int DoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, } if (ret == 0) { const byte* pubRawRef = NULL; - ret = GetStringRef(&pubRawSz, &pubRawRef, pk->publicKey, pk->publicKeySz, &i); + 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); + ret = wc_MlDsaKey_ImportPubRaw( + &keySig->ks.mldsa_composite.mldsa, pubRawRef, + params.mldsaPubSz); } if (ret == 0) { ret = ops->importPub(&keySig->ks.mldsa_composite.trad, @@ -21724,7 +22118,9 @@ static int DoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, 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); + byte* checkData = (byte*)WMALLOC( + UINT32_SZ + ssh->sessionIdSz + MSG_ID_SZ + dataToSignSz, + ssh->ctx->heap, DYNTYPE_TEMP); if (checkData == NULL) { ret = WS_MEMORY_E; } @@ -21771,7 +22167,8 @@ int wolfSSH_TestDoUserAuthRequestMlDsaComposite(WOLFSSH* ssh, if (authData == NULL) return WS_BAD_ARGUMENT; - return DoUserAuthRequestMlDsaComposite(ssh, &authData->sf.publicKey, authData, + return DoUserAuthRequestMlDsaComposite(ssh, &authData->sf.publicKey, + authData, keyId, pubKeyBlobSz); } @@ -21779,7 +22176,8 @@ int wolfSSH_TestPrepareUserAuthRequestMlDsaComposite(WOLFSSH* ssh, word32* payloadSz, const WS_UserAuthData* authData, WS_KeySignature* keySig) { - return PrepareUserAuthRequestMlDsaComposite(ssh, payloadSz, authData, keySig); + return PrepareUserAuthRequestMlDsaComposite(ssh, payloadSz, authData, + keySig); } /* exercises SignHMlDsaComposite() with a throwaway keypair; returns @@ -21804,15 +22202,16 @@ int wolfSSH_TestSignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, WMEMSET(&sigKey, 0, sizeof(sigKey)); sigKey.pubKeyId = keyId; - ret = wc_MlDsaKey_Init(&sigKey.sk.mldsa_composite.mldsa, + ret = wc_MlDsaKey_Init(&sigKey.sk.mldsa_composite.base.mldsa, ssh->ctx->heap, INVALID_DEVID); if (ret == 0) - ret = wc_MlDsaKey_SetParams(&sigKey.sk.mldsa_composite.mldsa, + ret = wc_MlDsaKey_SetParams(&sigKey.sk.mldsa_composite.base.mldsa, params.mldsaLevel); if (ret == 0) - ret = wc_MlDsaKey_MakeKey(&sigKey.sk.mldsa_composite.mldsa, ssh->rng); + ret = wc_MlDsaKey_MakeKey(&sigKey.sk.mldsa_composite.base.mldsa, + ssh->rng); if (ret != 0) { - wc_MlDsaKey_Free(&sigKey.sk.mldsa_composite.mldsa); + wc_MlDsaKey_Free(&sigKey.sk.mldsa_composite.base.mldsa); return WS_CRYPTO_FAILED; } @@ -21820,25 +22219,25 @@ int wolfSSH_TestSignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, ret = WS_UNIMPLEMENTED_E; } else { - ret = ops->init(&sigKey.sk.mldsa_composite.trad, ssh->ctx->heap); + ret = ops->init(&sigKey.sk.mldsa_composite.base.trad, ssh->ctx->heap); /* make_key not in CompositeTradOps: only this one call site needs it */ if (ret == 0) { if (params.tradType == TRAD_TYPE_ED25519) { #ifndef WOLFSSH_NO_ED25519 ret = wc_ed25519_make_key(ssh->rng, ED25519_KEY_SIZE, - &sigKey.sk.mldsa_composite.trad.ed25519); + &sigKey.sk.mldsa_composite.base.trad.ed25519); #endif } else if (params.tradType == TRAD_TYPE_ED448) { #ifdef HAVE_ED448 ret = wc_ed448_make_key(ssh->rng, 57, - &sigKey.sk.mldsa_composite.trad.ed448); + &sigKey.sk.mldsa_composite.base.trad.ed448); #endif } else if (params.tradType == TRAD_TYPE_ECC) { #ifndef WOLFSSH_NO_ECDSA ret = wc_ecc_make_key(ssh->rng, (int)params.tradPrivSz, - &sigKey.sk.mldsa_composite.trad.ecc); + &sigKey.sk.mldsa_composite.base.trad.ecc); #endif } } @@ -21848,9 +22247,9 @@ int wolfSSH_TestSignHMlDsaComposite(WOLFSSH* ssh, byte* sig, word32* sigSz, ret = SignHMlDsaComposite(ssh, sig, sigSz, &sigKey); } - wc_MlDsaKey_Free(&sigKey.sk.mldsa_composite.mldsa); + wc_MlDsaKey_Free(&sigKey.sk.mldsa_composite.base.mldsa); if (ops != NULL) { - ops->free(&sigKey.sk.mldsa_composite.trad); + ops->free(&sigKey.sk.mldsa_composite.base.trad); } return ret; diff --git a/src/keygen.c b/src/keygen.c index 1d4dacd9c..9f6d4cf03 100644 --- a/src/keygen.c +++ b/src/keygen.c @@ -53,6 +53,7 @@ #ifdef HAVE_ED448 #include #endif +#include #include #include #include @@ -339,14 +340,122 @@ int wolfSSH_MakeMlDsaKey(byte* out, word32 outSz, word32 level) } -/* builds an OpenSSH-key-v1 envelope (the only on-disk form for composite - * keys, which have no ASN.1 representation); see - * GetOpenSshKeyMlDsaComposite() in src/internal.c for the parser. - * 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 } - * pub is (mldsaPub || tradPub), priv is (mldsaSeed || tradPriv) */ +/* Builds the OpenSSH-key-v1 envelope (composite keys have no ASN.1 + * form); see GetOpenSshKeyMlDsaComposite() in internal.c for the parser. */ +#if !defined(WOLFSSH_NO_MLDSA) +/* Generates the traditional half of a composite key pair; exports raw + * material into the caller's buffers. */ +static int MakeCompositeTradKey(WC_RNG* rng, const CompositeParams* params, + byte* tradPub, byte* tradPriv) +{ + int ret = WS_NOT_COMPILED; + + WOLFSSH_UNUSED(rng); + WOLFSSH_UNUSED(tradPub); + WOLFSSH_UNUSED(tradPriv); + + if (params->tradType == TRAD_TYPE_ED25519) { +#ifndef WOLFSSH_NO_ED25519 + ed25519_key key; + word32 sz; + + if (wc_ed25519_init(&key) != 0) { + return WS_CRYPTO_FAILED; + } + ret = (wc_ed25519_make_key(rng, ED25519_KEY_SIZE, &key) == 0) ? + 0 : WS_CRYPTO_FAILED; + if (ret == 0) { + sz = params->tradPrivSz; + if (wc_ed25519_export_private_only(&key, tradPriv, &sz) != 0 || + sz != params->tradPrivSz) { + ret = WS_CRYPTO_FAILED; + } + } + if (ret == 0) { + sz = params->tradPubSz; + if (wc_ed25519_export_public(&key, tradPub, &sz) != 0 || + sz != params->tradPubSz) { + ret = WS_CRYPTO_FAILED; + } + } + wc_ed25519_free(&key); +#endif + } + else if (params->tradType == TRAD_TYPE_ED448) { +#ifdef HAVE_ED448 + ed448_key key; + word32 sz; + + if (wc_ed448_init(&key) != 0) { + return WS_CRYPTO_FAILED; + } + ret = (wc_ed448_make_key(rng, ED448_KEY_SIZE, &key) == 0) ? + 0 : WS_CRYPTO_FAILED; + if (ret == 0) { + sz = params->tradPrivSz; + if (wc_ed448_export_private_only(&key, tradPriv, &sz) != 0 || + sz != params->tradPrivSz) { + ret = WS_CRYPTO_FAILED; + } + } + if (ret == 0) { + sz = params->tradPubSz; + if (wc_ed448_export_public(&key, tradPub, &sz) != 0 || + sz != params->tradPubSz) { + ret = WS_CRYPTO_FAILED; + } + } + wc_ed448_free(&key); +#endif + } + else if (params->tradType == TRAD_TYPE_ECC) { +#ifndef WOLFSSH_NO_ECDSA + ecc_key key; + word32 sz; + /* Pin the curve explicitly: the draft requires P-256 for + * ML-DSA-44/65, P-384 for 87 -- key-size matching isn't guaranteed. */ + int curveId = (params->tradPrivSz <= 32) ? + ECC_SECP256R1 : ECC_SECP384R1; + + if (wc_ecc_init(&key) != 0) { + return WS_CRYPTO_FAILED; + } + ret = (wc_ecc_make_key_ex(rng, (int)params->tradPrivSz, &key, + curveId) == 0) ? 0 : WS_CRYPTO_FAILED; + if (ret == 0) { + sz = params->tradPrivSz; + if (wc_ecc_export_private_only(&key, tradPriv, &sz) != 0 || + sz != params->tradPrivSz) { + ret = WS_CRYPTO_FAILED; + } + } + if (ret == 0) { + sz = params->tradPubSz; + if (wc_ecc_export_x963(&key, tradPub, &sz) != 0 || + sz != params->tradPubSz) { + ret = WS_CRYPTO_FAILED; + } + } + wc_ecc_free(&key); +#endif + } + + return ret; +} +#endif /* !WOLFSSH_NO_MLDSA */ + +#if !defined(WOLFSSH_NO_MLDSA) +/* Base64_Encode() output size, mirrored from DoBase64_Encode(), so + * callers can size a buffer without a throwaway encode pass. */ +static word32 MlDsaCompositeBase64Sz(word32 fileSz) +{ + word32 chars = (fileSz + 2) / 3 * 4; + word32 lines = (chars + PEM_LINE_SZ - 1) / PEM_LINE_SZ; + + return chars + lines; +} +#endif /* !WOLFSSH_NO_MLDSA */ + int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, word32 tradType) { @@ -369,18 +478,6 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, 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; @@ -391,13 +488,24 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, 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 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; @@ -432,6 +540,19 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, + UINT32_SZ + pubBlobSz + UINT32_SZ + privKeysStrSz; + /* the encoded size depends only on fileSz, so it (and outSz + * feasibility) can be determined before doing any RNG/keygen work */ + b64Sz = MlDsaCompositeBase64Sz(fileSz); + + if (out == NULL) { + /* size query: caller wants the required buffer size only */ + return (int)(WSTRLEN(header) + b64Sz + WSTRLEN(footer)); + } + if (outSz < b64Sz + WSTRLEN(header) + WSTRLEN(footer)) { + WLOG(WS_LOG_DEBUG, "Output buffer too small for composite key"); + return WS_BUFFER_E; + } + ret = wc_InitRng(&rng); if (ret != 0) { WLOG(WS_LOG_DEBUG, "Couldn't create RNG"); @@ -467,85 +588,8 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, } 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) { + ret = MakeCompositeTradKey(&rng, ¶ms, tradPub, tradPriv); } if (ret != 0 && mldsaGenOk) { WLOG(WS_LOG_DEBUG, @@ -581,7 +625,8 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, 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; + WMEMCPY(tmpBuf + off, tradPub, params.tradPubSz); + off += params.tradPubSz; c32toa(privKeysStrSz, tmpBuf + off); off += UINT32_SZ; c32toa(checkint, tmpBuf + off); off += UINT32_SZ; @@ -591,7 +636,8 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, 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; + 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); @@ -605,37 +651,27 @@ int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, ret = WS_CRYPTO_FAILED; } else { - int b64Ret = Base64_Encode(tmpBuf, fileSz, NULL, &b64Sz); - if (b64Ret != 0 && b64Ret != WC_NO_ERR_TRACE(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; + /* outSz feasibility for this exact b64Sz was already verified + * against the deterministic size probe above */ + off = 0; + WMEMCPY(out + off, header, WSTRLEN(header)); + off += (word32)WSTRLEN(header); + if (Base64_Encode(tmpBuf, fileSz, out + off, &b64Sz) == 0) { + off += b64Sz; + WMEMCPY(out + off, footer, WSTRLEN(footer)); + off += (word32)WSTRLEN(footer); + /* out is PEM text of exactly `off` bytes; NUL-terminate + * only if the caller's buffer has room to spare */ + if (outSz > off) { + out[off] = '\0'; } - else ret = WS_CRYPTO_FAILED; + 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); WS_FORCEZERO(mldsaSeed, sizeof(mldsaSeed)); diff --git a/src/ossh.c b/src/ossh.c index 419e63f6f..8f7e89180 100644 --- a/src/ossh.c +++ b/src/ossh.c @@ -309,6 +309,8 @@ static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, word32 pubSz = 0; word32 privSz = 0; int ret; + int mldsaInit = 0; + int tradInit = 0; CompositeParams params; const CompositeTradOps* ops; @@ -317,25 +319,12 @@ static int GetOpenSshKeyMlDsaComposite(byte keyId, MlDsaKey* mldsa, ops = WS_GetTradOps(params.tradType); - /* caller doesn't clean up on error; each path below frees what it - * already initialized */ - ret = wc_MlDsaKey_Init(mldsa, heap, INVALID_DEVID); - if (ret != 0) { - return WS_CRYPTO_FAILED; - } - 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); + ret = InitCompositeKeyPair(¶ms, mldsa, tradKey, ops, heap, + &mldsaInit, &tradInit); if (ret != 0) { - wc_MlDsaKey_Free(mldsa); - return WS_CRYPTO_FAILED; + if (mldsaInit) wc_MlDsaKey_Free(mldsa); + if (tradInit) ops->free(tradKey); + return (ret == WS_UNIMPLEMENTED_E) ? ret : WS_CRYPTO_FAILED; } ret = GetStringRef(&pubSz, &pub, buf, len, idx); @@ -448,20 +437,8 @@ static int GetOpenSshKeyPublicMlDsaComposite(byte keyId, MlDsaKey* mldsa, 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; - } - } + ret = InitCompositeKeyPair(¶ms, mldsa, tradKey, ops, heap, + &mldsaInit, &tradInit); if (ret == 0) { ret = GetStringRef(&pubSz, &pub, buf, len, idx); @@ -483,7 +460,9 @@ static int GetOpenSshKeyPublicMlDsaComposite(byte keyId, MlDsaKey* mldsa, if (tradInit) { ops->free(tradKey); } - ret = WS_CRYPTO_FAILED; + if (ret != WS_UNIMPLEMENTED_E && ret != WS_KEY_FORMAT_E) { + ret = WS_CRYPTO_FAILED; + } } return ret; } @@ -553,7 +532,12 @@ int GetOpenSshPublicKey(WS_KeySignature *key, ret = GetOpenSshKeyPublicMlDsa(&key->ks.mldsa.key, buf, len, idx, WC_ML_DSA_87); break; + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: ret = GetOpenSshKeyPublicMlDsaComposite(keyId, &key->ks.mldsa_composite.mldsa, &key->ks.mldsa_composite.trad, @@ -576,11 +560,12 @@ int GetOpenSshKey(WS_KeySignature *key, const byte* buf, word32 len, word32* idx) { const char AuthMagic[] = "openssh-key-v1"; + const word32 authMagicSz = (word32)WSTRLEN(AuthMagic) + 1; /* incl NUL */ const byte* str = NULL; word32 keyCount = 0, strSz, i; int ret = WS_SUCCESS; - if (WSTRCMP(AuthMagic, (const char*)buf) != 0) { + if (len < authMagicSz || WMEMCMP(AuthMagic, buf, authMagicSz) != 0) { ret = WS_KEY_AUTH_MAGIC_E; } @@ -687,7 +672,12 @@ int GetOpenSshKey(WS_KeySignature *key, if (ret != WS_SUCCESS) key->keyId = ID_NONE; break; + case ID_MLDSA44_ES256: + case ID_MLDSA65_ES256: + case ID_MLDSA87_ES384: case ID_MLDSA44_ED25519: + case ID_MLDSA65_ED25519: + case ID_MLDSA87_ED448: ret = GetOpenSshKeyMlDsaComposite( key->keyId, &key->ks.mldsa_composite.mldsa, diff --git a/src/ssh.c b/src/ssh.c index fa7bfb60f..ebded94aa 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1796,11 +1796,9 @@ union wolfSSH_key { #endif }; -static const char* PrivBeginOpenSSH = "-----BEGIN OPENSSH PRIVATE KEY-----"; -static const char* PrivEndOpenSSH = "-----END OPENSSH PRIVATE KEY-----"; - #if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) /* currently only used in wolfSSH_ReadKey_file() */ + static const char* PrivBeginOpenSSH = "-----BEGIN OPENSSH PRIVATE KEY-----"; static const char* PrivBeginPrefix = "-----BEGIN "; /* static const char* PrivEndPrefix = "-----END "; */ static const char* PrivSuffix = " PRIVATE KEY-----"; @@ -2142,35 +2140,6 @@ static int DoOpenSshKey(const byte* in, word32 inSz, byte** out, int ret = WS_SUCCESS; byte* newKey = NULL; word32 newKeySz = inSz; /* binary will be smaller than PEM */ - word32 beginSz = (word32)WSTRLEN(PrivBeginOpenSSH); - word32 endSz = (word32)WSTRLEN(PrivEndOpenSSH); - const byte* b64 = NULL; - const char* footer = NULL; - word32 b64Sz = 0; - - /* Reject buffers too small to hold both markers. Without this guard the - * subtraction used to locate the base64 region underflows inSz. */ - if (inSz <= beginSz + endSz) { - WLOG(WS_LOG_DEBUG, "OpenSSH private key buffer too small"); - return WS_PARSE_E; - } - - /* The begin marker must lead the buffer. */ - if (WMEMCMP(in, PrivBeginOpenSSH, beginSz) != 0) { - WLOG(WS_LOG_DEBUG, "OpenSSH private key missing begin marker"); - return WS_PARSE_E; - } - - /* Locate the end marker so the base64 region is bounded by the input. */ - footer = WSTRNSTR((const char*)in + beginSz, PrivEndOpenSSH, - inSz - beginSz); - if (footer == NULL) { - WLOG(WS_LOG_DEBUG, "OpenSSH private key missing end marker"); - return WS_PARSE_E; - } - - b64 = in + beginSz; - b64Sz = (word32)(footer - (const char*)b64); if (*out == NULL) { newKey = (byte*)WMALLOC(newKeySz, heap, DYNTYPE_PRIVKEY); @@ -2187,17 +2156,18 @@ static int DoOpenSshKey(const byte* in, word32 inSz, byte** out, newKeySz = *outSz; } - ret = Base64_Decode((byte*)b64, b64Sz, newKey, &newKeySz); - if (ret == 0) { - ret = WS_SUCCESS; - } - else { - WLOG(WS_LOG_DEBUG, "Base64 decode of public key failed."); - ret = WS_PARSE_E; + /* locates the begin/end markers and base64-decodes the block between + * them; shared with wolfSSH_ProcessBuffer()'s OPENSSH format path */ + ret = WS_StripOpenSshPem(in, inSz, newKey, &newKeySz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "OpenSSH private key marker/decode failed."); } if (ret == WS_SUCCESS) { ret = IdentifyOpenSshKey(newKey, newKeySz, heap); + if (ret <= 0) { + WLOG(WS_LOG_DEBUG, "Unable to identify key"); + } } if (ret > 0) { @@ -2208,7 +2178,6 @@ static int DoOpenSshKey(const byte* in, word32 inSz, byte** out, ret = WS_SUCCESS; } else { - WLOG(WS_LOG_DEBUG, "Unable to identify key"); WS_FORCEZERO(newKey, newKeySz); if (*out == NULL) { WFREE(newKey, heap, DYNTYPE_PRIVKEY); diff --git a/tests/kex.c b/tests/kex.c index 1059f15a8..756b61541 100644 --- a/tests/kex.c +++ b/tests/kex.c @@ -515,13 +515,37 @@ int wolfSSH_KexTest(int argc, char** argv) #ifndef WOLFSSH_NO_MLDSA87 AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa-87"), EXIT_SUCCESS); #endif -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) /* Uses the "@openssh.com" wire name that OpenSSH negotiates for this * algorithm, matching what wolfSSH now emits (see NameIdMap). */ AssertIntEQ( wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa44-ed25519@openssh.com"), EXIT_SUCCESS); #endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa44-es256"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa65-es256"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa65-ed25519"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa87-es384"), + EXIT_SUCCESS); +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + AssertIntEQ(wolfSSH_KexTest_MlDsaHostKey("ssh-mldsa87-ed448"), + EXIT_SUCCESS); +#endif AssertIntEQ(wolfSSH_Cleanup(), WS_SUCCESS); diff --git a/tests/unit.c b/tests/unit.c index 704bf9927..174f235eb 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -898,7 +898,7 @@ static int test_MlDsaKeyGen(void) printf("MlDsaKeyGen: MakeMlDsaKey level %s failed (%d)\n", params[i].name, sz); WFREE(der, NULL, DYNTYPE_BUFFER); - result = -106; + result = -107; break; } /* Confirm the DER size constant is exact, not merely an upper @@ -918,7 +918,7 @@ static int test_MlDsaKeyGen(void) printf("MlDsaKeyGen: undersized buffer wrong result %d, level %s\n", sz, params[i].name); WFREE(der, NULL, DYNTYPE_BUFFER); - result = -107; + result = -109; break; } @@ -929,7 +929,7 @@ static int test_MlDsaKeyGen(void) int sz = wolfSSH_MakeMlDsaKey(NULL, 0, 9999); if (sz != WS_BAD_ARGUMENT) { printf("MlDsaKeyGen: invalid level wrong result %d\n", sz); - result = -108; + result = -111; } } @@ -942,14 +942,36 @@ static int test_MlDsaKeyGen(void) * parser to confirm the two agree on the on-disk envelope layout. */ static int test_MlDsaCompositeKeyGen(void) { + /* NULL-terminated so the table is never empty if ECDSA and + * Ed25519/Ed448 are all disabled while ML-DSA is enabled */ static const struct { word32 level; word32 tradType; const char* name; } params[] = { - #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) { WOLFSSH_MLDSAKEY_44, WOLFSSH_COMPOSITE_TRAD_ED25519, "44+Ed25519" }, #endif + #if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + { WOLFSSH_MLDSAKEY_44, WOLFSSH_COMPOSITE_TRAD_ECDSA, "44+ES256" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + { WOLFSSH_MLDSAKEY_65, WOLFSSH_COMPOSITE_TRAD_ED25519, "65+Ed25519" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + { WOLFSSH_MLDSAKEY_65, WOLFSSH_COMPOSITE_TRAD_ECDSA, "65+ES256" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + { WOLFSSH_MLDSAKEY_87, WOLFSSH_COMPOSITE_TRAD_ED448, "87+Ed448" }, + #endif + #if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + { WOLFSSH_MLDSAKEY_87, WOLFSSH_COMPOSITE_TRAD_ECDSA, "87+ES384" }, + #endif + { 0, 0, NULL } }; const word32 bufSz = 8192; word32 i; @@ -958,7 +980,8 @@ static int test_MlDsaCompositeKeyGen(void) word32 firstSz = 0; int result = 0; - for (i = 0; i < (word32)(sizeof(params) / sizeof(params[0])); i++) { + for (i = 0; i < (word32)(sizeof(params) / sizeof(params[0])) && + params[i].name != NULL; i++) { WOLFSSH_CTX* ctx; byte* buf; int sz; @@ -967,7 +990,7 @@ static int test_MlDsaCompositeKeyGen(void) if (buf == NULL) { printf("MlDsaCompositeKeyGen: alloc failed for %s\n", params[i].name); - result = -109; + result = -120; break; } @@ -977,7 +1000,7 @@ static int test_MlDsaCompositeKeyGen(void) printf("MlDsaCompositeKeyGen: MakeMlDsaCompositeKey %s " "failed (%d)\n", params[i].name, sz); WFREE(buf, NULL, DYNTYPE_BUFFER); - result = -109; + result = -121; break; } @@ -992,7 +1015,7 @@ static int test_MlDsaCompositeKeyGen(void) printf("MlDsaCompositeKeyGen: CTX_new failed for %s\n", params[i].name); WFREE(buf, NULL, DYNTYPE_BUFFER); - result = -110; + result = -122; break; } @@ -1000,7 +1023,7 @@ static int test_MlDsaCompositeKeyGen(void) WOLFSSH_FORMAT_OPENSSH) != WS_SUCCESS) { printf("MlDsaCompositeKeyGen: round-trip parse failed for %s\n", params[i].name); - result = -111; + result = -123; } wolfSSH_CTX_free(ctx); @@ -1011,21 +1034,25 @@ static int test_MlDsaCompositeKeyGen(void) } if (result == 0) { + /* out == NULL is a size query for a valid (level, tradType): it + * should return the required buffer size, not an error. */ int sz = wolfSSH_MakeMlDsaCompositeKey(NULL, 0, firstLevel, firstTradType); - if (sz != WS_BAD_ARGUMENT) { - printf("MlDsaCompositeKeyGen: NULL out wrong result %d\n", sz); - result = -112; + if (sz <= 0 || (word32)sz != firstSz) { + printf("MlDsaCompositeKeyGen: NULL out size query wrong " + "result %d (expected %u)\n", sz, firstSz); + result = -124; } } if (result == 0) { byte dummy[1]; - int sz = wolfSSH_MakeMlDsaCompositeKey(dummy, sizeof(dummy), 9999, 9999); + 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; + result = -125; } } @@ -1036,20 +1063,20 @@ static int test_MlDsaCompositeKeyGen(void) if (sz != WS_BAD_ARGUMENT) { printf("MlDsaCompositeKeyGen: mismatched level/tradType wrong " "result %d\n", sz); - result = -115; + result = -127; } } /* firstLevel/firstTradType/firstSz are only populated when params[] has * at least one entry for this build's enabled algorithms; skip the * undersized-buffer check (which relies on a valid firstSz) otherwise. */ - if (result == 0 && (sizeof(params) / sizeof(params[0])) > 0) { + if (result == 0 && firstSz > 0) { byte* buf = (byte*)WMALLOC(firstSz, NULL, DYNTYPE_BUFFER); if (buf == NULL) { printf("MlDsaCompositeKeyGen: alloc failed for undersized " "buffer test\n"); - result = -114; + result = -126; } else { int sz = wolfSSH_MakeMlDsaCompositeKey(buf, firstSz - 1, @@ -1057,7 +1084,7 @@ static int test_MlDsaCompositeKeyGen(void) if (sz != WS_BUFFER_E) { printf("MlDsaCompositeKeyGen: undersized buffer wrong " "result %d\n", sz); - result = -115; + result = -128; } WFREE(buf, NULL, DYNTYPE_BUFFER); } @@ -1065,9 +1092,110 @@ static int test_MlDsaCompositeKeyGen(void) return result; } -#endif -#endif +#endif /* WOLFSSH_NO_MLDSA */ + +#endif /* WOLFSSH_KEYGEN */ + +/* Exercises the malformed-input error paths of the WOLFSSH_FORMAT_OPENSSH + * PEM stripping added to wolfSSH_ProcessBuffer() (via the public + * wolfSSH_CTX_UsePrivateKey_buffer() entry point): a header that doesn't + * match "-----BEGIN OPENSSH PRIVATE KEY-----", a missing footer, and a + * corrupt/non-Base64 body. Each should fail with WS_BAD_FILE_E rather than + * crash or leak. */ +static int test_OpenSshPemNegative(void) +{ + static const char badHeader[] = + "-----BEGIN OPENSSH PUBLIC KEY-----\n" + "AAAA\n" + "-----END OPENSSH PRIVATE KEY-----\n"; + static const char noFooter[] = + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "AAAA\n"; + static const char badBase64[] = + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "not valid base64 !!!\n" + "-----END OPENSSH PRIVATE KEY-----\n"; + static const struct { + const char* pem; + word32 pemSz; + const char* name; + } cases[] = { + { badHeader, (word32)sizeof(badHeader) - 1, "bad header" }, + { noFooter, (word32)sizeof(noFooter) - 1, "missing footer" }, + { badBase64, (word32)sizeof(badBase64) - 1, "bad base64" }, + }; + word32 i; + int result = 0; + + for (i = 0; i < (word32)(sizeof(cases) / sizeof(cases[0])); i++) { + WOLFSSH_CTX* ctx; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { + printf("OpenSshPemNegative: CTX_new failed for %s\n", + cases[i].name); + result = -116; + break; + } + + ret = wolfSSH_CTX_UsePrivateKey_buffer(ctx, + (const byte*)cases[i].pem, cases[i].pemSz, + WOLFSSH_FORMAT_OPENSSH); + if (ret != WS_BAD_FILE_E) { + printf("OpenSshPemNegative: %s wrong result %d\n", + cases[i].name, ret); + result = -117; + } + + wolfSSH_CTX_free(ctx); + if (result != 0) { + break; + } + } + + return result; +} + + +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 +/* A well-formed OpenSSH-format key that isn't a composite type must be + * rejected, not stored as an envelope no non-composite parser can walk. */ +static int test_OpenSshFormatNonCompositeRejected(void) +{ + static const char ecdsaOpenSsh[] = + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS\n" + "1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTAqdBgCp8bYSq2kQQ48/Ud8Iy6Mjnb\n" + "/fpB3LfSE/1kx9VaaE4FL3i9Gg2vDV0eLGM3PWksFNPhULxtcYJyjaBjAAAAqJAeleSQHp\n" + "XkAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMCp0GAKnxthKraR\n" + "BDjz9R3wjLoyOdv9+kHct9IT/WTH1VpoTgUveL0aDa8NXR4sYzc9aSwU0+FQvG1xgnKNoG\n" + "MAAAAgPrOgktioNqad/wHNC/rt/zVrpNqDnOwg9tNDFMOTwo8AAAANYm9iQGxvY2FsaG9z\n" + "dAECAw==\n" + "-----END OPENSSH PRIVATE KEY-----\n"; + WOLFSSH_CTX* ctx; + int ret; + int result = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { + printf("OpenSshFormatNonCompositeRejected: CTX_new failed\n"); + return -118; + } + + ret = wolfSSH_CTX_UsePrivateKey_buffer(ctx, + (const byte*)ecdsaOpenSsh, (word32)sizeof(ecdsaOpenSsh) - 1, + WOLFSSH_FORMAT_OPENSSH); + if (ret != WS_UNIMPLEMENTED_E) { + printf("OpenSshFormatNonCompositeRejected: wrong result %d\n", ret); + result = -119; + } + + wolfSSH_CTX_free(ctx); + return result; +} +#endif /* !WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ #if defined(WOLFSSH_TEST_INTERNAL) && \ @@ -8366,6 +8494,9 @@ static int test_DoUserAuthRequestMlDsa(void) return 0; } +/* tamperSig: 0 = no tampering, 1 = flip a byte in the trad-algorithm + * signature component, 2 = flip a byte in the ML-DSA signature component. + * Either tamper mode must be rejected by verification. */ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, byte keyId, int tamperSig) { @@ -8439,16 +8570,20 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, 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; } + 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 (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; } + if (wc_ed25519_make_key(&rng, ED25519_KEY_SIZE, + &ed25519Key) != 0) { result = -708; goto done; } #else result = -709; goto done; #endif @@ -8457,17 +8592,19 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, #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; } + 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; + 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; } + if (wc_ecc_make_key(&rng, keysz, + &eccKey) != 0) { result = -714; goto done; } #else result = -715; goto done; #endif @@ -8477,7 +8614,8 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, 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; } + if (wc_MlDsaKey_ExportPubRaw(&signingKey, pubRaw, + &sz) != 0 || sz != params.mldsaPubSz) { result = -717; goto done; } } tradPub = (byte*)WMALLOC(params.tradPubSz, NULL, 0); @@ -8485,34 +8623,42 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, 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; } + 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; } + 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; } + 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; + 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; + 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; } @@ -8535,7 +8681,8 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, off = 0; MlDsaTest_PutLen(checkData + off, ssh->sessionIdSz); off += UINT32_SZ; - WMEMCPY(checkData + off, ssh->sessionId, ssh->sessionIdSz); off += ssh->sessionIdSz; + WMEMCPY(checkData + off, ssh->sessionId, + ssh->sessionIdSz); off += ssh->sessionIdSz; checkData[off++] = MSGID_USERAUTH_REQUEST; WMEMCPY(checkData + off, dataToSign, dataToSignSz); @@ -8546,33 +8693,40 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, 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_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); + 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); + 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) { + 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; } + 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; } + 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) { @@ -8585,18 +8739,31 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, 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); + 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_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; + if (wc_ecc_sig_to_rs(asnSig, asnSigSz, rBuf, &rSz, sBuf, + &sSz) != 0) { result = -734; goto done; } + { + /* mpint pad, mirroring CompositeEccSign()'s wire format. */ + byte rPad = (rBuf[0] & 0x80) ? 1 : 0; + byte sPad = (sBuf[0] & 0x80) ? 1 : 0; + + off = 0; + MlDsaTest_PutLen(tradSig + off, rSz + rPad); off += UINT32_SZ; + if (rPad) + tradSig[off++] = 0; + WMEMCPY(tradSig + off, rBuf, rSz); off += rSz; + MlDsaTest_PutLen(tradSig + off, sSz + sPad); off += UINT32_SZ; + if (sPad) + tradSig[off++] = 0; + WMEMCPY(tradSig + off, sBuf, sSz); off += sSz; + tradSigSz = off; + } #endif } @@ -8611,12 +8778,20 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, WMEMCPY(sigBlob + off, mldsaSig, mldsaSigSz); off += mldsaSigSz; WMEMCPY(sigBlob + off, tradSig, tradSigSz); off += tradSigSz; - if (tamperSig) { + if (tamperSig == 1) { /* Flip a byte in the trad-algorithm component of the signature * (last byte of the blob) and confirm verification rejects it * rather than accepting a corrupted composite signature. */ sigBlob[sigBlobSz - 1] ^= 0xFF; } + else if (tamperSig == 2) { + /* Flip the first byte of the ML-DSA component of the signature + * and confirm verification rejects it too; a bug that verifies + * only the trad half (or mis-slices the combined signature) + * would let this slip through. */ + word32 mldsaOff = UINT32_SZ * 2 + keyTypeNameSz; + sigBlob[mldsaOff] ^= 0xFF; + } WMEMSET(&authData, 0, sizeof(authData)); authData.type = WOLFSSH_USERAUTH_PUBLICKEY; @@ -8634,7 +8809,8 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, authData.sf.publicKey.signature = sigBlob; authData.sf.publicKey.signatureSz = sigBlobSz; - ret = wolfSSH_TestDoUserAuthRequestMlDsaComposite(ssh, &authData, keyId, pubKeyBlobSz); + ret = wolfSSH_TestDoUserAuthRequestMlDsaComposite(ssh, &authData, keyId, + pubKeyBlobSz); if (tamperSig) { if (ret == WS_SUCCESS) { printf("DoUserAuthRequestMlDsaComposite (%s) tampered sig " @@ -8643,7 +8819,8 @@ static int test_DoUserAuthRequestMlDsaComposite_Params(const char* keyTypeName, } } else if (ret != WS_SUCCESS) { - printf("DoUserAuthRequestMlDsaComposite (%s) failed: ret=%d\n", keyTypeName, ret); + printf("DoUserAuthRequestMlDsaComposite (%s) failed: ret=%d\n", + keyTypeName, ret); result = -736; } @@ -8739,9 +8916,9 @@ static int test_SignHMlDsaComposite_Params(const char* label, byte keyId) 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). */ + /* Historical KEX_SIG_SIZE reserved only 64 bytes of trad headroom, + * too small for P-384/Ed448. Exercises the same generic tradSigSz + * check as above, not the KEX_SIG_SIZE constant itself. */ if (params.tradSigSz > 64) { sigSz = params.mldsaSigSz + 64; ret = wolfSSH_TestSignHMlDsaComposite(ssh, sig, &sigSz, keyId); @@ -8761,8 +8938,38 @@ 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_ED25519) - ret = test_SignHMlDsaComposite_Params("ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519); +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa44-es256", + ID_MLDSA44_ES256); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa65-es256", + ID_MLDSA65_ES256); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa87-es384", + ID_MLDSA87_ES384); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa44-ed25519@openssh.com", + ID_MLDSA44_ED25519); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa65-ed25519", + ID_MLDSA65_ED25519); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ret = test_SignHMlDsaComposite_Params("ssh-mldsa87-ed448", + ID_MLDSA87_ED448); if (ret != 0) return ret; #endif return 0; @@ -8775,7 +8982,8 @@ static int test_SignHMlDsaComposite(void) * undetected in earlier revisions. */ static int test_PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope(void) { -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) static const char keyTypeName[] = "ssh-mldsa44-ed25519@openssh.com"; static const char magic[] = "openssh-key-v1"; static const char none[] = "none"; @@ -8828,10 +9036,13 @@ static int test_PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope(void) 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; } + 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; } + 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; } @@ -8895,7 +9106,8 @@ static int test_PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope(void) if (file == NULL) { result = -967; goto done; } off = 0; - WMEMCPY(file + off, magic, WSTRLEN(magic) + 1); off += (word32)WSTRLEN(magic) + 1; + 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; @@ -9044,8 +9256,8 @@ static int test_BuildUserAuthRequestMlDsaComposite_Params( 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); + printf("BuildUserAuthRequestMlDsaComposite (%s): " + "ReadKey_buffer failed ret=%d\n", keyTypeName, ret); result = -984; goto done; } WMEMCPY(privKey, decodedPrivKey, decodedPrivKeySz); @@ -9183,11 +9395,45 @@ static int test_BuildUserAuthRequestMlDsaComposite_Params( static int test_BuildUserAuthRequestMlDsaComposite(void) { int ret = 0; -#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-es256", WOLFSSH_MLDSAKEY_44, + WOLFSSH_COMPOSITE_TRAD_ECDSA, ID_MLDSA44_ES256); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa65-es256", WOLFSSH_MLDSAKEY_65, + WOLFSSH_COMPOSITE_TRAD_ECDSA, ID_MLDSA65_ES256); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa87-es384", WOLFSSH_MLDSAKEY_87, + WOLFSSH_COMPOSITE_TRAD_ECDSA, ID_MLDSA87_ES384); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) ret = test_BuildUserAuthRequestMlDsaComposite_Params( "ssh-mldsa44-ed25519@openssh.com", WOLFSSH_MLDSAKEY_44, WOLFSSH_COMPOSITE_TRAD_ED25519, ID_MLDSA44_ED25519); if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa65-ed25519", WOLFSSH_MLDSAKEY_65, + WOLFSSH_COMPOSITE_TRAD_ED25519, ID_MLDSA65_ED25519); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ret = test_BuildUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa87-ed448", WOLFSSH_MLDSAKEY_87, + WOLFSSH_COMPOSITE_TRAD_ED448, ID_MLDSA87_ED448); + if (ret != 0) return ret; #endif return 0; } @@ -9196,10 +9442,74 @@ static int test_BuildUserAuthRequestMlDsaComposite(void) 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 !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256", + ID_MLDSA44_ES256, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256", + ID_MLDSA44_ES256, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-es256", + ID_MLDSA44_ES256, 2); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256", + ID_MLDSA65_ES256, 0); if (ret != 0) return ret; - ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 1); + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256", + ID_MLDSA65_ES256, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-es256", + ID_MLDSA65_ES256, 2); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384", + ID_MLDSA87_ES384, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384", + ID_MLDSA87_ES384, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-es384", + ID_MLDSA87_ES384, 2); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params( + "ssh-mldsa44-ed25519@openssh.com", ID_MLDSA44_ED25519, 2); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \ + !defined(NO_SHA512) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519", + ID_MLDSA65_ED25519, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519", + ID_MLDSA65_ED25519, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa65-ed25519", + ID_MLDSA65_ED25519, 2); + if (ret != 0) return ret; +#endif +#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448) + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448", + ID_MLDSA87_ED448, 0); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448", + ID_MLDSA87_ED448, 1); + if (ret != 0) return ret; + ret = test_DoUserAuthRequestMlDsaComposite_Params("ssh-mldsa87-ed448", + ID_MLDSA87_ED448, 2); if (ret != 0) return ret; #endif return 0; @@ -12983,7 +13293,8 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); testResult = testResult || (unitResult != 0); unitResult = test_PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope(); - printf("PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope: %s (result=%d)\n", + printf("PrepareUserAuthRequestMlDsaComposite_OpenSshEnvelope: " + "%s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); testResult = testResult || (unitResult != 0); #ifdef WOLFSSH_KEYGEN @@ -13210,6 +13521,16 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; #endif +#endif /* WOLFSSH_KEYGEN */ + unitResult = test_OpenSshPemNegative(); + printf("OpenSshPemNegative: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + unitResult = test_OpenSshFormatNonCompositeRejected(); + printf("OpenSshFormatNonCompositeRejected: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; #endif wolfSSH_Cleanup(); diff --git a/wolfssh/error.h b/wolfssh/error.h index ff3a4b607..531c4a816 100644 --- a/wolfssh/error.h +++ b/wolfssh/error.h @@ -138,8 +138,9 @@ enum WS_ErrorCodes { WS_KDF_E = -1097, /* KDF error*/ WS_DISCONNECT = -1098, /* peer sent disconnect */ WS_MLDSA_E = -1099, /* MLDSA failure */ + WS_ED448_E = -1100, /* Ed448 failure */ - WS_LAST_E = WS_MLDSA_E /* Last error indicator */ + WS_LAST_E = WS_ED448_E /* Update to indicate last error */ }; diff --git a/wolfssh/internal.h b/wolfssh/internal.h index dfdd5b89c..4d0429782 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -462,7 +462,12 @@ enum { ID_MLDSA65, ID_MLDSA87, /* always declared; NameIdMap/WS_GetCompositeParams() gate reachability */ + ID_MLDSA44_ES256, + ID_MLDSA65_ES256, + ID_MLDSA87_ES384, ID_MLDSA44_ED25519, + ID_MLDSA65_ED25519, + ID_MLDSA87_ED448, #endif ID_X509V3_SSH_RSA, ID_X509V3_ECDSA_SHA2_NISTP256, @@ -1252,6 +1257,31 @@ WOLFSSH_LOCAL int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId); WOLFSSH_LOCAL int wolfSSH_FwdWorker(WOLFSSH* ssh); +#ifndef WOLFSSH_NO_MLDSA +/* Shared shape for a composite key's ML-DSA+traditional pair; reused + * in src/internal.c so the copies can't drift apart. */ +typedef struct WS_MlDsaCompositeBody { + MlDsaKey mldsa; + union { +#ifndef WOLFSSH_NO_ECDSA + ecc_key ecc; +#endif +#ifndef WOLFSSH_NO_ED25519 + ed25519_key ed25519; +#endif +#ifdef HAVE_ED448 + ed448_key ed448; +#endif +#if defined(WOLFSSH_NO_ECDSA) && defined(WOLFSSH_NO_ED25519) && \ + !defined(HAVE_ED448) + /* keep union non-empty (empty union rejected by some + * compilers) though unusable without a trad component */ + byte placeholder; +#endif + } trad; +} WS_MlDsaCompositeBody; +#endif /* WOLFSSH_NO_MLDSA */ + typedef struct WS_KeySignature { byte keyId; byte sigId; @@ -1281,29 +1311,8 @@ typedef struct WS_KeySignature { struct { MlDsaKey key; } mldsa; + WS_MlDsaCompositeBody mldsa_composite; #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) - /* keep union non-empty (empty union rejected by some - * compilers) though unusable without a trad component */ - byte placeholder; -#endif - } trad; - } mldsa_composite; -#endif } ks; } WS_KeySignature; @@ -1329,11 +1338,12 @@ typedef struct WS_KeySignature { #else #define COMPOSITE_MAX_TRAD_PRIV_SZ ECC_P384_COORD_SZ #endif -/* worst-case trad signature size: Ed448, else P-384 raw r/s */ +/* worst-case trad signature size: Ed448, else P-384 raw r/s wire format + * (length-prefixed r and s, each with up to 1 byte of sign-byte padding) */ #ifdef HAVE_ED448 #define COMPOSITE_MAX_TRAD_SIG_SZ ED448_SIG_SIZE #else -#define COMPOSITE_MAX_TRAD_SIG_SZ (2 * ECC_P384_COORD_SZ) +#define COMPOSITE_MAX_TRAD_SIG_SZ (2 * (LENGTH_SZ + ECC_P384_COORD_SZ + 1)) #endif /* defensive slack on top of BuildUserAuthRequestMlDsaComposite()'s * worst-case signature size; not load-bearing */ @@ -1376,7 +1386,13 @@ typedef struct 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); +/* Inits both halves of a composite key pair; mldsaInit/tradInit track + * which succeeded so the caller can clean up correctly on failure. */ +WOLFSSH_LOCAL int InitCompositeKeyPair(const CompositeParams* params, + MlDsaKey* mldsa, void* tradKey, const CompositeTradOps* ops, + void* heap, int* mldsaInit, int* tradInit); +WOLFSSH_LOCAL int WS_Hash_Helper(enum wc_HashType hashId, const byte* msg, + word32 msgSz, byte* hash, word32 hashSz); #endif WOLFSSH_LOCAL int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, @@ -1389,6 +1405,8 @@ WOLFSSH_LOCAL int GetOpenSshKey(WS_KeySignature *key, WOLFSSH_LOCAL int GetOpenSshPublicKey(WS_KeySignature *key, const byte* buf, word32 len, word32* idx); #endif +WOLFSSH_LOCAL int WS_StripOpenSshPem(const byte* in, word32 inSz, + byte* out, word32* outSz); /* Parsing functions */ @@ -1824,8 +1842,8 @@ enum WS_MessageIdLimits { 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, + 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); diff --git a/wolfssh/keygen.h b/wolfssh/keygen.h index f0031535a..7cba8ee9f 100644 --- a/wolfssh/keygen.h +++ b/wolfssh/keygen.h @@ -46,11 +46,8 @@ 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. */ +/* Traditional algorithm paired with the ML-DSA level; not every pair is + * valid -- see WS_GetCompositeParams() in internal.c for the list. */ #define WOLFSSH_COMPOSITE_TRAD_ECDSA 1 #define WOLFSSH_COMPOSITE_TRAD_ED25519 2 #define WOLFSSH_COMPOSITE_TRAD_ED448 3 @@ -61,6 +58,8 @@ WOLFSSH_API int wolfSSH_MakeRsaKey(byte* out, word32 outSz, WOLFSSH_API int wolfSSH_MakeEcdsaKey(byte* out, word32 outSz, word32 size); WOLFSSH_API int wolfSSH_MakeEd25519Key(byte* out, word32 outSz, word32 size); WOLFSSH_API int wolfSSH_MakeMlDsaKey(byte* out, word32 outSz, word32 level); +/* Writes an OpenSSH-format PEM key for the given level/trad pairing; + * out == NULL queries required size. Returns length or negative WS_*. */ WOLFSSH_API int wolfSSH_MakeMlDsaCompositeKey(byte* out, word32 outSz, word32 level, word32 tradType);