Skip to content

Commit c5d283b

Browse files
ejohnstownphilljj
authored andcommitted
fix: compare RSA blocks instead of parsing
RFC 8332 section 3 advises against verifying RSASSA-PKCS1-v1_5 by applying the key and parsing the output. wolfSSH_RsaVerify() builds the expected EMSA-PKCS1-v1_5 block, applies the key with a raw public operation, and compares whole modulus-sized blocks. - verify through wc_RsaFunction() and a constant-time block compare - reject a signature not less than the modulus, which the raw operation would otherwise reduce into a second encoding of the same signature - reject a key too small for the digest, and a wc_RsaEncryptSize() error - carve the four working blocks from one allocation - add test_RsaVerify_BadPadding() and test_RsaVerify_SigRange() Offload builds leave the modulus empty, so the range check is skipped there. That check walks the bytes itself rather than calling memcmp(), which some libcs compare as signed char, ordering the modulus's high byte backwards. The padding cases pass against the old code as well; the range case does not. Issue: F-10574
1 parent bfe6fe0 commit c5d283b

2 files changed

Lines changed: 368 additions & 31 deletions

File tree

src/internal.c

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14701,6 +14701,21 @@ static INLINE byte SigTypeForId(byte id)
1470114701

1470214702

1470314703
#ifndef WOLFSSH_NO_RSA
14704+
/* Big-endian magnitude compare. Not memcmp(): Zephyr's minimal libc walks it
14705+
* as signed char, which orders a modulus's high byte backwards. */
14706+
static int BigEndianCompare(const byte* a, const byte* b, word32 sz)
14707+
{
14708+
word32 idx;
14709+
14710+
for (idx = 0; idx < sz; idx++) {
14711+
if (a[idx] != b[idx]) {
14712+
return (a[idx] > b[idx]) ? 1 : -1;
14713+
}
14714+
}
14715+
return 0;
14716+
}
14717+
14718+
1470414719
/*
1470514720
* wolfSSH_RsaVerify
1470614721
* sig - signature to verify
@@ -14711,43 +14726,54 @@ static INLINE byte SigTypeForId(byte id)
1471114726
* heap - allocation heap
1471214727
* loc - calling function for logging
1471314728
*
14714-
* Takes the provided digest of type digestId and converts it to an
14715-
* encoded digest. Then verifies the signature, comparing the output
14716-
* digest and compares it.
14729+
* Rebuilds the expected EMSA-PKCS1-v1_5 block from encDigest, applies the
14730+
* public key to the signature, and compares the two blocks whole. Per RFC
14731+
* 8332 section 3, the recovered value is never parsed.
1471714732
*/
1471814733
int wolfSSH_RsaVerify(const byte *sig, word32 sigSz,
1471914734
const byte* encDigest, word32 encDigestSz,
1472014735
RsaKey* key, void* heap, const char* loc)
1472114736
{
14737+
byte* scratch = NULL;
1472214738
byte* checkSig = NULL;
14723-
int checkDigestSz;
14739+
byte* checkBlock = NULL;
14740+
byte* encBlock = NULL;
14741+
byte* nBlock = NULL;
1472414742
word32 keySz;
14743+
int encryptSz;
1472514744
int ret = WS_SUCCESS;
14726-
#ifdef WOLFSSH_SMALL_STACK
14727-
byte* checkDigest = NULL;
14728-
#else
14729-
byte checkDigest[MAX_ENCODED_SIG_SZ];
14730-
#endif
1473114745

1473214746
if (sig == NULL) {
1473314747
WLOG(WS_LOG_DEBUG, "%s: %s", loc, "Missing RSA signature");
1473414748
return WS_RSA_E;
1473514749
}
1473614750

14737-
keySz = (word32)wc_RsaEncryptSize(key);
14751+
encryptSz = wc_RsaEncryptSize(key);
14752+
if (encryptSz <= 0) {
14753+
WLOG(WS_LOG_DEBUG, "%s: %s", loc, "Bad RSA key size");
14754+
return WS_RSA_E;
14755+
}
14756+
keySz = (word32)encryptSz;
1473814757

14739-
if (ret == WS_SUCCESS) {
14740-
checkSig = (byte*)WMALLOC(keySz, heap, DYNTYPE_TEMP);
14741-
if (checkSig == NULL)
14742-
ret = WS_MEMORY_E;
14758+
/* Room for the header, the trailing zero, and eight pad bytes. */
14759+
if (keySz < encDigestSz + RSA_MIN_PAD_SZ) {
14760+
WLOG(WS_LOG_DEBUG, "%s: %s", loc, "RSA key too small for the digest");
14761+
return WS_RSA_E;
1474314762
}
14744-
#ifdef WOLFSSH_SMALL_STACK
14763+
14764+
/* One block each: normalized signature, recovered value, expected
14765+
* encoding, modulus. */
1474514766
if (ret == WS_SUCCESS) {
14746-
checkDigest = (byte*)WMALLOC(MAX_ENCODED_SIG_SZ, heap, DYNTYPE_TEMP);
14747-
if (checkDigest == NULL)
14767+
scratch = (byte*)WMALLOC(keySz * 4, heap, DYNTYPE_TEMP);
14768+
if (scratch == NULL)
1474814769
ret = WS_MEMORY_E;
14770+
else {
14771+
checkSig = scratch;
14772+
checkBlock = scratch + keySz;
14773+
encBlock = scratch + (keySz * 2);
14774+
nBlock = scratch + (keySz * 3);
14775+
}
1474914776
}
14750-
#endif
1475114777

1475214778
/* Normalize the peer's signature. Some SSH implementations remove
1475314779
* leading zeros on the signatures they encode. We need to pad the
@@ -14767,28 +14793,55 @@ int wolfSSH_RsaVerify(const byte *sig, word32 sigSz,
1476714793
WMEMCPY(checkSig + offset, sig, sigSz);
1476814794
}
1476914795

14796+
/* SP 800-56B wants 1 < s < n-1. wc_RsaFunction() only checks that for a
14797+
* decrypt, and the modexp would reduce s mod n, so s + n verifies as a
14798+
* second encoding of the same signature. Offload builds leave n empty and
14799+
* have nothing to compare against. */
14800+
if (ret == WS_SUCCESS && mp_count_bits(&key->n) != 0) {
14801+
word32 eSz = keySz, nSz = keySz;
14802+
14803+
/* checkBlock is scratch until the modexp below fills it. */
14804+
if (wc_RsaFlattenPublicKey(key, checkBlock, &eSz, nBlock, &nSz) != 0
14805+
|| nSz != keySz) {
14806+
WLOG(WS_LOG_DEBUG, "%s: %s", loc, "Bad RSA public key");
14807+
ret = WS_RSA_E;
14808+
}
14809+
else if (BigEndianCompare(checkSig, nBlock, keySz) >= 0) {
14810+
WLOG(WS_LOG_DEBUG, "%s: %s", loc, "RSA signature out of range");
14811+
ret = WS_RSA_E;
14812+
}
14813+
}
14814+
14815+
/* Expected block: 0x00 0x01 || 0xFF pad || 0x00 || DigestInfo. */
1477014816
if (ret == WS_SUCCESS) {
14771-
volatile int sizeCompare;
14772-
volatile int compare;
14817+
word32 padSz = keySz - encDigestSz - 3;
14818+
word32 idx = 0;
14819+
14820+
encBlock[idx++] = 0x00;
14821+
encBlock[idx++] = 0x01;
14822+
WMEMSET(encBlock + idx, 0xFF, padSz);
14823+
idx += padSz;
14824+
encBlock[idx++] = 0x00;
14825+
WMEMCPY(encBlock + idx, encDigest, encDigestSz);
14826+
}
1477314827

14774-
checkDigestSz = wc_RsaSSL_Verify(checkSig, keySz,
14775-
checkDigest, MAX_ENCODED_SIG_SZ, key);
14828+
/* RFC 8332 section 3: compare whole blocks, don't parse. */
14829+
if (ret == WS_SUCCESS) {
14830+
word32 checkBlockSz = keySz;
14831+
int rsaRet;
1477614832

14777-
sizeCompare = checkDigestSz > 0 && encDigestSz != (word32)checkDigestSz;
14778-
compare = ConstantCompare(encDigest, checkDigest, encDigestSz);
14833+
rsaRet = wc_RsaFunction(checkSig, keySz, checkBlock, &checkBlockSz,
14834+
RSA_PUBLIC_ENCRYPT, key, NULL);
1477914835

14780-
if (checkDigestSz < 0 || sizeCompare || compare) {
14836+
if (rsaRet != 0 || checkBlockSz != keySz
14837+
|| ConstantCompare(checkBlock, encBlock, keySz) != 0) {
1478114838
WLOG(WS_LOG_DEBUG, "%s: %s", loc, "Bad RSA Verify");
1478214839
ret = WS_RSA_E;
1478314840
}
1478414841
}
1478514842

14786-
#ifdef WOLFSSH_SMALL_STACK
14787-
if (checkDigest)
14788-
WFREE(checkDigest, heap, DYNTYPE_TEMP);
14789-
#endif
14790-
if (checkSig)
14791-
WFREE(checkSig, heap, DYNTYPE_TEMP);
14843+
if (scratch)
14844+
WFREE(scratch, heap, DYNTYPE_TEMP);
1479214845
WOLFSSH_UNUSED(loc); /* Unused when WLOG is not defined */
1479314846
return ret;
1479414847
}

0 commit comments

Comments
 (0)