Skip to content

Commit b12a2a5

Browse files
committed
Fix Ed25519 user-auth verify and add unit test
- Map ed25519 verify_msg_final errors to WS_CRYPTO_FAILED and status==0 to WS_ED25519_E (no longer overwritten) - Expose DoUserAuthRequestEd25519 via test shim - Add unit test covering valid and tampered signatures Issue: F-3445
1 parent dcd9bd4 commit b12a2a5

3 files changed

Lines changed: 243 additions & 8 deletions

File tree

src/internal.c

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8006,14 +8006,14 @@ static int DoUserAuthRequestEd25519(WOLFSSH* ssh,
80068006
key_ptr);
80078007
}
80088008

8009-
if(ret == WS_SUCCESS) {
8009+
if (ret == WS_SUCCESS) {
80108010
temp[0] = MSGID_USERAUTH_REQUEST;
80118011
ret = wc_ed25519_verify_msg_update(temp, MSG_ID_SZ, key_ptr);
80128012
}
80138013

80148014
/* The rest of the fields in the signature are already
80158015
* in the buffer. Just need to account for the sizes. */
8016-
if(ret == WS_SUCCESS) {
8016+
if (ret == WS_SUCCESS) {
80178017
ret = wc_ed25519_verify_msg_update(pk->dataToSign,
80188018
authData->usernameSz +
80198019
authData->serviceNameSz +
@@ -8022,16 +8022,17 @@ static int DoUserAuthRequestEd25519(WOLFSSH* ssh,
80228022
(UINT32_SZ * 5), key_ptr);
80238023
}
80248024

8025-
if(ret == WS_SUCCESS) {
8025+
if (ret == WS_SUCCESS) {
80268026
int status = 0;
8027-
ret = wc_ed25519_verify_msg_final(pk->signature + i, sz,
8027+
int verifyRet = wc_ed25519_verify_msg_final(pk->signature + i, sz,
80288028
&status, key_ptr);
8029-
if (ret != 0) {
8030-
WLOG(WS_LOG_DEBUG, "Could not verify signature");
8029+
if (verifyRet != 0) {
8030+
WLOG(WS_LOG_DEBUG,
8031+
"DUAREd: Signature Verify fail (%d)", verifyRet);
80318032
ret = WS_CRYPTO_FAILED;
80328033
}
8033-
else
8034-
ret = status ? WS_SUCCESS : WS_ED25519_E;
8034+
else if (status == 0)
8035+
ret = WS_ED25519_E;
80358036
}
80368037

80378038
if (key_ptr) {
@@ -18095,4 +18096,18 @@ int wolfSSH_TestRsaVerify(const byte* sig, word32 sigSz,
1809518096

1809618097
#endif /* !WOLFSSH_NO_RSA */
1809718098

18099+
#ifndef WOLFSSH_NO_ED25519
18100+
18101+
int wolfSSH_TestDoUserAuthRequestEd25519(WOLFSSH* ssh,
18102+
WS_UserAuthData* authData)
18103+
{
18104+
if (authData == NULL) {
18105+
return WS_BAD_ARGUMENT;
18106+
}
18107+
18108+
return DoUserAuthRequestEd25519(ssh, &authData->sf.publicKey, authData);
18109+
}
18110+
18111+
#endif /* !WOLFSSH_NO_ED25519 */
18112+
1809818113
#endif /* WOLFSSH_TEST_INTERNAL */

tests/unit.c

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,6 +1692,214 @@ static int test_RsaVerify_BadDigest(void)
16921692
}
16931693

16941694
#endif /* !WOLFSSH_NO_RSA */
1695+
1696+
#if !defined(WOLFSSH_NO_ED25519) && defined(HAVE_ED25519) && \
1697+
defined(HAVE_ED25519_SIGN) && defined(HAVE_ED25519_VERIFY) && \
1698+
defined(WOLFSSL_ED25519_STREAMING_VERIFY)
1699+
1700+
/* Locally-generated Ed25519 keypair for the DoUserAuthRequestEd25519 test.
1701+
* 32-byte private seed followed by the 32-byte raw public key. Created with
1702+
* ssh-keygen and decoded from the OpenSSH private key format so the test is
1703+
* deterministic and does not depend on an RNG. */
1704+
static const byte unitTestEd25519Priv[32] = {
1705+
0x05, 0xf5, 0x9c, 0x02, 0x55, 0x93, 0x32, 0x93,
1706+
0xb9, 0xc2, 0x2e, 0xa7, 0x20, 0x05, 0x33, 0x0c,
1707+
0x40, 0xcd, 0xfa, 0xff, 0x73, 0xe4, 0x4a, 0xe1,
1708+
0x50, 0x2a, 0x4b, 0x37, 0x20, 0x66, 0xc5, 0x56
1709+
};
1710+
static const byte unitTestEd25519Pub[32] = {
1711+
0x33, 0x76, 0xaf, 0x20, 0x97, 0xce, 0x38, 0xdf,
1712+
0x5a, 0x76, 0x62, 0xfc, 0xb2, 0x87, 0x6e, 0x9d,
1713+
0xd3, 0x9e, 0x85, 0x87, 0xf3, 0x0e, 0x72, 0x6c,
1714+
0x1e, 0xc0, 0x01, 0xe2, 0x81, 0x96, 0xb8, 0x49
1715+
};
1716+
1717+
/* Write a 32-bit big-endian length prefix at out. */
1718+
static void Ed25519Test_PutLen(byte* out, word32 v)
1719+
{
1720+
out[0] = (byte)(v >> 24);
1721+
out[1] = (byte)(v >> 16);
1722+
out[2] = (byte)(v >> 8);
1723+
out[3] = (byte)(v);
1724+
}
1725+
1726+
/* DoUserAuthRequestEd25519 unit test
1727+
*
1728+
* Drives DoUserAuthRequestEd25519 directly with a fully-formed Ed25519
1729+
* USERAUTH_REQUEST and asserts (1) a correct signature returns WS_SUCCESS
1730+
* and (2) a tampered signature returns either WS_ED25519_E or
1731+
* WS_CRYPTO_FAILED. This is the test that makes the
1732+
* `status ? WS_SUCCESS : WS_ED25519_E` style mapping load-bearing -- without
1733+
* the negative case any mutation that hard-codes WS_SUCCESS or inverts the
1734+
* status would silently survive.
1735+
*
1736+
* The Ed25519 verifier in wolfSSL has two failure paths reachable from this
1737+
* function: wc_ed25519_verify_msg_final returns non-zero (WS_CRYPTO_FAILED)
1738+
* or returns zero with status=0 (WS_ED25519_E). Different wolfSSL versions
1739+
* have routed bad signatures through either branch; we accept either failure
1740+
* code so the test stays portable while still killing the most dangerous
1741+
* mutation -- turning either error into WS_SUCCESS.
1742+
*/
1743+
static int test_DoUserAuthRequestEd25519(void)
1744+
{
1745+
static const char keyTypeName[] = "ssh-ed25519";
1746+
static const char username[] = "wolfssh";
1747+
static const char serviceName[] = "ssh-connection";
1748+
static const char authName[] = "publickey";
1749+
const word32 keyTypeNameSz = (word32)(sizeof(keyTypeName) - 1);
1750+
const word32 usernameSz = (word32)(sizeof(username) - 1);
1751+
const word32 serviceNameSz = (word32)(sizeof(serviceName) - 1);
1752+
const word32 authNameSz = (word32)(sizeof(authName) - 1);
1753+
1754+
WOLFSSH_CTX* ctx = NULL;
1755+
WOLFSSH* ssh = NULL;
1756+
ed25519_key signingKey;
1757+
int signingKeyInit = 0;
1758+
WS_UserAuthData authData;
1759+
byte pubKeyBlob[64];
1760+
byte sigBlob[128];
1761+
byte badSigBlob[128];
1762+
byte dataToSign[256];
1763+
byte checkData[512];
1764+
byte sig[ED25519_SIG_SIZE];
1765+
word32 pubKeyBlobSz = 0;
1766+
word32 sigBlobSz = 0;
1767+
word32 dataToSignSz = 0;
1768+
word32 checkDataSz = 0;
1769+
word32 sigSz = sizeof(sig);
1770+
word32 off;
1771+
int result = 0;
1772+
int ret;
1773+
1774+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
1775+
if (ctx == NULL) return -600;
1776+
ssh = wolfSSH_new(ctx);
1777+
if (ssh == NULL) { result = -601; goto done; }
1778+
1779+
/* Stub a session id so the verify hash has something to absorb. */
1780+
ssh->sessionIdSz = 16;
1781+
WMEMSET(ssh->sessionId, 0xA5, ssh->sessionIdSz);
1782+
1783+
/* Load the embedded Ed25519 keypair for signing. */
1784+
if (wc_ed25519_init(&signingKey) != 0) { result = -602; goto done; }
1785+
signingKeyInit = 1;
1786+
if (wc_ed25519_import_private_key(unitTestEd25519Priv,
1787+
(word32)sizeof(unitTestEd25519Priv),
1788+
unitTestEd25519Pub, (word32)sizeof(unitTestEd25519Pub),
1789+
&signingKey) != 0) {
1790+
result = -603; goto done;
1791+
}
1792+
1793+
/* Build the SSH public key blob: string "ssh-ed25519" || string pubkey */
1794+
off = 0;
1795+
Ed25519Test_PutLen(pubKeyBlob + off, keyTypeNameSz); off += UINT32_SZ;
1796+
WMEMCPY(pubKeyBlob + off, keyTypeName, keyTypeNameSz);
1797+
off += keyTypeNameSz;
1798+
Ed25519Test_PutLen(pubKeyBlob + off,
1799+
(word32)sizeof(unitTestEd25519Pub));
1800+
off += UINT32_SZ;
1801+
WMEMCPY(pubKeyBlob + off, unitTestEd25519Pub,
1802+
sizeof(unitTestEd25519Pub));
1803+
off += (word32)sizeof(unitTestEd25519Pub);
1804+
pubKeyBlobSz = off;
1805+
1806+
/* Build the dataToSign region the same way the wire packet would lay it
1807+
* out: username || service || authmethod || hasSig=1 || pkAlgo || pkBlob.
1808+
* DoUserAuthRequestEd25519 hashes exactly this region. */
1809+
off = 0;
1810+
Ed25519Test_PutLen(dataToSign + off, usernameSz); off += UINT32_SZ;
1811+
WMEMCPY(dataToSign + off, username, usernameSz); off += usernameSz;
1812+
Ed25519Test_PutLen(dataToSign + off, serviceNameSz); off += UINT32_SZ;
1813+
WMEMCPY(dataToSign + off, serviceName, serviceNameSz);
1814+
off += serviceNameSz;
1815+
Ed25519Test_PutLen(dataToSign + off, authNameSz); off += UINT32_SZ;
1816+
WMEMCPY(dataToSign + off, authName, authNameSz); off += authNameSz;
1817+
dataToSign[off++] = 1; /* hasSignature */
1818+
Ed25519Test_PutLen(dataToSign + off, keyTypeNameSz); off += UINT32_SZ;
1819+
WMEMCPY(dataToSign + off, keyTypeName, keyTypeNameSz);
1820+
off += keyTypeNameSz;
1821+
Ed25519Test_PutLen(dataToSign + off, pubKeyBlobSz); off += UINT32_SZ;
1822+
WMEMCPY(dataToSign + off, pubKeyBlob, pubKeyBlobSz); off += pubKeyBlobSz;
1823+
dataToSignSz = off;
1824+
1825+
/* Build the bytes that get signed: sessionIdSz || sessionId ||
1826+
* MSGID_USERAUTH_REQUEST || dataToSign. Mirrors BuildUserAuthRequestEd25519
1827+
* on the client side. */
1828+
off = 0;
1829+
Ed25519Test_PutLen(checkData + off, ssh->sessionIdSz); off += UINT32_SZ;
1830+
WMEMCPY(checkData + off, ssh->sessionId, ssh->sessionIdSz);
1831+
off += ssh->sessionIdSz;
1832+
checkData[off++] = MSGID_USERAUTH_REQUEST;
1833+
WMEMCPY(checkData + off, dataToSign, dataToSignSz);
1834+
off += dataToSignSz;
1835+
checkDataSz = off;
1836+
1837+
if (wc_ed25519_sign_msg(checkData, checkDataSz, sig, &sigSz,
1838+
&signingKey) != 0) {
1839+
result = -604; goto done;
1840+
}
1841+
1842+
/* Build the SSH signature blob: string "ssh-ed25519" || string sig */
1843+
off = 0;
1844+
Ed25519Test_PutLen(sigBlob + off, keyTypeNameSz); off += UINT32_SZ;
1845+
WMEMCPY(sigBlob + off, keyTypeName, keyTypeNameSz);
1846+
off += keyTypeNameSz;
1847+
Ed25519Test_PutLen(sigBlob + off, sigSz); off += UINT32_SZ;
1848+
WMEMCPY(sigBlob + off, sig, sigSz); off += sigSz;
1849+
sigBlobSz = off;
1850+
1851+
/* Populate authData the way DoUserAuthRequest/DoUserAuthRequestPublicKey
1852+
* would before dispatching to DoUserAuthRequestEd25519. */
1853+
WMEMSET(&authData, 0, sizeof(authData));
1854+
authData.type = WOLFSSH_USERAUTH_PUBLICKEY;
1855+
authData.username = (const byte*)username;
1856+
authData.usernameSz = usernameSz;
1857+
authData.serviceName = (const byte*)serviceName;
1858+
authData.serviceNameSz = serviceNameSz;
1859+
authData.authName = (const byte*)authName;
1860+
authData.authNameSz = authNameSz;
1861+
authData.sf.publicKey.dataToSign = dataToSign;
1862+
authData.sf.publicKey.publicKeyType = (const byte*)keyTypeName;
1863+
authData.sf.publicKey.publicKeyTypeSz = keyTypeNameSz;
1864+
authData.sf.publicKey.publicKey = pubKeyBlob;
1865+
authData.sf.publicKey.publicKeySz = pubKeyBlobSz;
1866+
authData.sf.publicKey.hasSignature = 1;
1867+
authData.sf.publicKey.signature = sigBlob;
1868+
authData.sf.publicKey.signatureSz = sigBlobSz;
1869+
1870+
/* Positive case: untouched signature must verify. */
1871+
ret = wolfSSH_TestDoUserAuthRequestEd25519(ssh, &authData);
1872+
if (ret != WS_SUCCESS) {
1873+
printf("DoUserAuthRequestEd25519 positive: ret=%d (expected %d)\n",
1874+
ret, WS_SUCCESS);
1875+
result = -605; goto done;
1876+
}
1877+
1878+
/* Negative case: flip a byte inside the raw signature (skip past the
1879+
* 4 + keyTypeNameSz + 4 header so we land in the actual signature
1880+
* material). Must NOT return WS_SUCCESS. */
1881+
WMEMCPY(badSigBlob, sigBlob, sigBlobSz);
1882+
badSigBlob[UINT32_SZ + keyTypeNameSz + UINT32_SZ + 10] ^= 0xFF;
1883+
authData.sf.publicKey.signature = badSigBlob;
1884+
1885+
ret = wolfSSH_TestDoUserAuthRequestEd25519(ssh, &authData);
1886+
if (ret != WS_ED25519_E && ret != WS_CRYPTO_FAILED) {
1887+
printf("DoUserAuthRequestEd25519 tampered: ret=%d\n", ret);
1888+
result = -606; goto done;
1889+
}
1890+
1891+
done:
1892+
if (signingKeyInit)
1893+
wc_ed25519_free(&signingKey);
1894+
if (ssh != NULL)
1895+
wolfSSH_free(ssh);
1896+
if (ctx != NULL)
1897+
wolfSSH_CTX_free(ctx);
1898+
return result;
1899+
}
1900+
1901+
#endif /* Ed25519 verify test guards */
1902+
16951903
#endif /* WOLFSSH_TEST_INTERNAL */
16961904

16971905
/* Error Code And Message Test */
@@ -1801,6 +2009,14 @@ int wolfSSH_UnitTest(int argc, char** argv)
18012009
printf("RsaVerify_BadDigest: %s\n",
18022010
(unitResult == 0 ? "SUCCESS" : "FAILED"));
18032011
testResult = testResult || unitResult;
2012+
#endif
2013+
#if !defined(WOLFSSH_NO_ED25519) && defined(HAVE_ED25519) && \
2014+
defined(HAVE_ED25519_SIGN) && defined(HAVE_ED25519_VERIFY) && \
2015+
defined(WOLFSSL_ED25519_STREAMING_VERIFY)
2016+
unitResult = test_DoUserAuthRequestEd25519();
2017+
printf("DoUserAuthRequestEd25519: %s\n",
2018+
(unitResult == 0 ? "SUCCESS" : "FAILED"));
2019+
testResult = testResult || unitResult;
18042020
#endif
18052021
unitResult = test_ChannelPutData();
18062022
printf("ChannelPutData: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED"));

wolfssh/internal.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,10 @@ enum WS_MessageIdLimits {
13721372
const byte* encDigest, word32 encDigestSz,
13731373
RsaKey* key, void* heap);
13741374
#endif /* !WOLFSSH_NO_RSA */
1375+
#ifndef WOLFSSH_NO_ED25519
1376+
WOLFSSH_API int wolfSSH_TestDoUserAuthRequestEd25519(WOLFSSH* ssh,
1377+
WS_UserAuthData* authData);
1378+
#endif /* !WOLFSSH_NO_ED25519 */
13751379
#endif /* WOLFSSH_TEST_INTERNAL */
13761380

13771381
/* dynamic memory types */

0 commit comments

Comments
 (0)