Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/scp-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
id: cache-wolfssl
with:
path: build-dir/
key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}
key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}-v3
lookup-only: true

- name: Checkout, build, and install wolfssl
Expand Down Expand Up @@ -73,7 +73,7 @@ jobs:
uses: actions/cache@v5
with:
path: build-dir/
key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}
key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}-v3
fail-on-cache-miss: true

- uses: actions/checkout@v6
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/sshd-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ jobs:
matrix:
os: [ ubuntu-latest ]
wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }}
mldsa: [ 'yes', 'no' ]
include:
- mldsa: 'yes'
mldsa_flag: '--enable-mldsa'
- mldsa: 'no'
mldsa_flag: ''
name: Build wolfssl
runs-on: ${{ matrix.os }}
timeout-minutes: 4
Expand All @@ -42,7 +48,7 @@ jobs:
id: cache-wolfssl
with:
path: build-dir/
key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}
key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}-mldsa-${{ matrix.mldsa }}-v3
lookup-only: true

- name: Checkout, build, and install wolfssl
Expand All @@ -52,7 +58,7 @@ jobs:
repository: wolfssl/wolfssl
ref: ${{ matrix.wolfssl }}
path: wolfssl
configure: --enable-all
configure: --enable-all ${{ matrix.mldsa_flag }}
check: false
install: true

Expand All @@ -65,6 +71,7 @@ jobs:
matrix:
os: [ ubuntu-latest ]
wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }}
mldsa: [ 'yes', 'no' ]
name: Build and test wolfsshd
runs-on: ${{ matrix.os }}
timeout-minutes: 10
Expand All @@ -73,7 +80,7 @@ jobs:
uses: actions/cache@v5
with:
path: build-dir/
key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}
key: wolfssh-sshd-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }}-mldsa-${{ matrix.mldsa }}-v3
fail-on-cache-miss: true

- uses: actions/checkout@v6
Expand Down
110 changes: 103 additions & 7 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -1690,12 +1690,18 @@ void wolfSSH_KEY_clean(WS_KeySignature* key)
* fails try to load it as if ECDSA. Both public and private keys can be
* decoded. For RSA keys, the key format is described as "ssh-rsa".
*
* Private-only ML-DSA keys are rejected (WS_CRYPTO_FAILED) as public keys
* cannot be derived. ECDSA derives and validates the public key here.
* Ed25519 allows missing public keys if HAVE_ED25519_MAKE_KEY is defined
* (derived later at KEX); otherwise rejected like ML-DSA.
*
* @param in key to identify
* @param inSz size of key
* @param isPrivate indicates private or public key
* @param heap heap to use for memory allocation
* @param pkey optionally return populated WS_KeySignature
* @return keyId as int, WS_MEMORY_E, WS_UNIMPLEMENTED_E
* @return keyId as int, WS_MEMORY_E, WS_UNIMPLEMENTED_E,
* WS_CRYPTO_FAILED
*/
int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap,
WS_KeySignature **pkey)
Expand All @@ -1704,6 +1710,9 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap,
word32 idx;
int ret;
int dynType = isPrivate ? DYNTYPE_PRIVKEY : DYNTYPE_PUBKEY;
/* Set to WS_CRYPTO_FAILED if ML-DSA key lacks derivable public key.
* Prevents Ed25519 fallback decode. */
int noPubKeyRet = 0;
#ifndef WOLFSSH_NO_MLDSA
Comment thread
stenslae marked this conversation as resolved.
byte mlDsaLevel = 0;
int mlDsaInit = 0;
Expand Down Expand Up @@ -1765,6 +1774,8 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap,

/* If decode was successful, this is an ECDSA key. */
if (ret == 0) {
int curveSupported = 1;

switch (wc_ecc_get_curve_id(key->ks.ecc.key.idx)) {
case ECC_SECP256R1:
key->keyId = ID_ECDSA_SHA2_NISTP256;
Expand All @@ -1776,8 +1787,29 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap,
key->keyId = ID_ECDSA_SHA2_NISTP521;
break;
default:
/* Not a supported curve, so free the key */
wc_ecc_free(&key->ks.ecc.key);
curveSupported = 0;
break;
}

/* SEC1 allows omitted public point; derive it now so
* an undecodable key is rejected at load time, not
* KEX. */
if (curveSupported && isPrivate &&
key->ks.ecc.key.type == ECC_PRIVATEKEY_ONLY) {
if (wc_ecc_make_pub(&key->ks.ecc.key, NULL) != 0) {
WLOG(WS_LOG_ERROR,
"ECDSA priv-only key rejected; no "
"derivable pubkey");
curveSupported = 0;
ret = WS_CRYPTO_FAILED;
noPubKeyRet = ret;
key->keyId = ID_UNKNOWN;
}
}

if (!curveSupported) {
/* Not a supported curve, so free the key */
wc_ecc_free(&key->ks.ecc.key);
}
} else {
wc_ecc_free(&key->ks.ecc.key);
Expand All @@ -1786,7 +1818,7 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap,
}
#endif /* WOLFSSH_NO_ECDSA */
#ifndef WOLFSSH_NO_MLDSA
if (key->keyId == ID_UNKNOWN) {
if (key->keyId == ID_UNKNOWN && noPubKeyRet == 0) {
idx = 0;
mlDsaLevel = 0;
mlDsaInit = 0;
Expand All @@ -1796,6 +1828,17 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap,
if (isPrivate) {
ret = wc_MlDsaKey_PrivateKeyDecode(&key->ks.mldsa.key,
in, inSz, &idx);
if (ret == 0) {
/* Priv-only decode can succeed with no derivable
* public key; reject here instead of at first
* handshake. */
if (!key->ks.mldsa.key.pubKeySet) {
WLOG(WS_LOG_ERROR,
"ML-DSA priv-only key rejected; no derivable pubkey");
ret = WS_CRYPTO_FAILED;
noPubKeyRet = ret;
}
}
}
else {
/* PublicKeyDecode auto-detects level from SPKI OID. */
Expand Down Expand Up @@ -1857,14 +1900,33 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap,
}
#endif /* WOLFSSH_NO_MLDSA */
#if !defined(WOLFSSH_NO_ED25519)
if (key->keyId == ID_UNKNOWN) {
/* noPubKeyRet == 0 check: don't reinterpret rejected ML-DSA bytes
* as Ed25519. */
if (key->keyId == ID_UNKNOWN && noPubKeyRet == 0) {
idx = 0;
ret = wc_ed25519_init_ex(&key->ks.ed25519.key, heap, INVALID_DEVID);

if (ret == 0) {
if (isPrivate) {
ret = wc_Ed25519PrivateKeyDecode(in, &idx,
&key->ks.ed25519.key, inSz);
if (ret == 0) {
/* No embedded pubkey. SendKexGetSigningKey derives
* it via wc_ed25519_make_public() if available;
* otherwise reject now instead of failing at KEX. */
if (!key->ks.ed25519.key.pubKeySet) {
#ifdef HAVE_ED25519_MAKE_KEY
WLOG(WS_LOG_WARN,
"Ed25519 priv-only key; pubkey derives at KEX");
#else
WLOG(WS_LOG_ERROR,
"Ed25519 priv-only key rejected; no "
"derivable pubkey");
ret = WS_CRYPTO_FAILED;
noPubKeyRet = ret;
#endif /* HAVE_ED25519_MAKE_KEY */
}
}
}
else {
ret = wc_Ed25519PublicKeyDecode(in, &idx,
Expand All @@ -1882,7 +1944,8 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap,
#endif /* WOLFSSH_NO_ED25519 */

if (key->keyId == ID_UNKNOWN) {
ret = WS_UNIMPLEMENTED_E;
/* Prefer specific rejection reason over generic fallback. */
ret = (noPubKeyRet != 0) ? noPubKeyRet : WS_UNIMPLEMENTED_E;
}
else {
if (pkey != NULL)
Expand Down Expand Up @@ -13125,6 +13188,22 @@ static int SendKexGetSigningKey(WOLFSSH* ssh,
ret = wc_ecc_export_x963(&sigKeyBlock_ptr->sk.ecc.key,
sigKeyBlock_ptr->sk.ecc.q,
&sigKeyBlock_ptr->sk.ecc.qSz);
if (ret != 0 && sigKeyBlock_ptr->sk.ecc.key.type ==
ECC_PRIVATEKEY_ONLY) {
/* Priv-only SEC1 DER: ECDSA public key is derivable
* from private scalar. wc_ecc_make_pub() is
* unconditional and caches result. Retry export. */
sigKeyBlock_ptr->sk.ecc.qSz =
(word32)sizeof(sigKeyBlock_ptr->sk.ecc.q);
ret = wc_ecc_make_pub(&sigKeyBlock_ptr->sk.ecc.key,
NULL);
if (ret == 0) {
ret = wc_ecc_export_x963(
&sigKeyBlock_ptr->sk.ecc.key,
sigKeyBlock_ptr->sk.ecc.q,
&sigKeyBlock_ptr->sk.ecc.qSz);
}
}
PRIVATE_KEY_LOCK();
}
/* Hash in the length of the public key block. */
Expand Down Expand Up @@ -13182,10 +13261,27 @@ static int SendKexGetSigningKey(WOLFSSH* ssh,
if (ret == 0)
ret = wc_Ed25519PrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, &scratch, &sigKeyBlock_ptr->sk.ed.key, ssh->ctx->privateKey[keyIdx].keySz);

if (ret == 0)
if (ret == 0) {
ret = wc_ed25519_export_public(&sigKeyBlock_ptr->sk.ed.key,
sigKeyBlock_ptr->sk.ed.q,
&sigKeyBlock_ptr->sk.ed.qSz );
#ifdef HAVE_ED25519_MAKE_KEY
if (ret != 0 && !sigKeyBlock_ptr->sk.ed.key.pubKeySet) {
/* Priv-only DER: Ed25519 public key is deterministically
* derivable from private seed. */
sigKeyBlock_ptr->sk.ed.qSz = ED25519_PUB_KEY_SIZE;
ret = wc_ed25519_make_public(&sigKeyBlock_ptr->sk.ed.key,
sigKeyBlock_ptr->sk.ed.q, ED25519_PUB_KEY_SIZE);
if (ret == 0) {
/* trusted=1: q derived from same private scalar;
* untrusted path re-verification redundant. */
ret = wc_ed25519_import_public_ex(
sigKeyBlock_ptr->sk.ed.q, ED25519_PUB_KEY_SIZE,
&sigKeyBlock_ptr->sk.ed.key, 1);
}
}
#endif /* HAVE_ED25519_MAKE_KEY */
}

/* Hash in the length of the public key block. */
if (ret == 0) {
Expand Down
Loading
Loading