Add ML-DSA44-Ed25519 composite signature support

pull/1157/head
Emma Stensland 2026-07-16 14:31:24 -06:00 committed by John Safranek
parent b2b32cca51
commit 866f7392cd
14 changed files with 3600 additions and 60 deletions

View File

@ -53,6 +53,7 @@
#include <wolfssl/wolfcrypt/wc_port.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
#include <wolfssl/wolfcrypt/coding.h>
#include <wolfssl/wolfcrypt/asn_public.h>
#ifdef WOLFSSL_FPKI
#include <wolfssl/wolfcrypt/asn.h>
@ -160,14 +161,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
@ -270,9 +298,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;
@ -289,8 +318,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;
}
@ -1342,6 +1372,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,

View File

@ -108,6 +108,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);

View File

@ -2318,18 +2318,19 @@ static int test_ScanShadowFile_truncatedLine(void)
#endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */
#ifdef WOLFSSL_BASE64_ENCODE
/* Build a mutable "ssh-rsa <base64(key)>" line; WSTRTOK mutates in place. */
static int BuildAuthKeysLine(const byte* key, word32 keySz,
char* lineOut, word32 lineOutSz)
/* Build a mutable "<type> <base64(key)>" 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) {
@ -2343,7 +2344,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;
@ -5346,6 +5451,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_PermitRootProhibitPassword),
@ -5374,8 +5621,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),

View File

@ -353,6 +353,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;
@ -415,20 +416,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
@ -438,36 +432,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) {

View File

@ -1861,8 +1861,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;
@ -3414,13 +3476,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 */

View File

@ -27,7 +27,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 \

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,15 @@
#ifndef WOLFSSH_NO_ECDSA
#include <wolfssl/wolfcrypt/ecc.h>
#endif
#ifndef WOLFSSH_NO_ED25519
#include <wolfssl/wolfcrypt/ed25519.h>
#endif
#ifdef HAVE_ED448
#include <wolfssl/wolfcrypt/ed448.h>
#endif
#include <wolfssl/wolfcrypt/asn_public.h>
#include <wolfssl/wolfcrypt/coding.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
#ifdef WOLFSSH_KEYGEN
@ -362,6 +370,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, &params) != 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 */

View File

@ -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, &params);
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, &params);
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:
@ -571,7 +727,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;
}

View File

@ -518,6 +518,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);

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,9 @@
#include <wolfssl/wolfcrypt/rsa.h>
#include <wolfssl/wolfcrypt/curve25519.h>
#include <wolfssl/wolfcrypt/ed25519.h>
#ifdef HAVE_ED448
#include <wolfssl/wolfcrypt/ed448.h>
#endif
#ifndef WOLFSSL_WOLFSSH
#error "wolfssh requires wolfSSL built with WOLFSSL_WOLFSSH"
@ -468,6 +471,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,
@ -1315,9 +1320,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);
@ -1772,6 +1871,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,

View File

@ -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