I have written a raw (textbook) RSA implementation (just for fun) and I wonder is there an easy way to make it secure enough so it can be used in real life (without implementing OAEP+ and RSASSA-PSS)? Are there any simple algorithms for padding and generating secure digital signatures?
3 Answers
Actually, RSASSA-PKCS1-v1_5 signature padding is quite simple, and has no known weaknesses (the similarly named RSAES-PKCS1-v1_5 padding is broken for encryption unless implemented in a very very careful way; don't use that).
The padding format is:
00 01 FF FF FF ... FF FF 00 <DER of Hash Type> <Hash>
where DER of Hash Type is a byte string that depends on the type of hash you used, and Hash is the output of the hash function. For SHA-256, the DER is the byte string:
30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20
So, you take your hash, prepend it with a fixed string, and you're done.
If you want to make things a bit simpler, you could omit the DER of Hash Type (which would mean that you're not precisely PKCS #1.5, however it does not introduce any known weaknesses.
- 154,064
- 12
- 239
- 382
There is also a more simple but not well-known signature scheme for RSA;
- RSA Full Domain Hash
It has existentially unforgeable under adaptive chosen-message attacks in the random oracle model.
- Introduced in How to sign given any trapdoor permutation by M. Bellare and S. Micali, in 1992,
- Analyzed in The Exact Security of Digital Signatures How to Sign with RSA and Rabin by Mihir Bellare and Phillip Rogaway in 1998.
- Best security bounds given in On the exact security of Full Domain Hash by Jean-Sebastien Coron, in 2000.
Today RSA-FDH is very simple;
- Sign: $\sigma = Sign(H, m) = (H(m))^d \bmod n$
- Verify: $\{0,1\} = Verify(H, m, \sigma) = [\sigma^e \bmod n \overset{?}= H(m) \bmod n]$
It was not easy to sign then because of the size requirement; the hash $H$ must have an output size equal to RSA modulus size. Now, the obvious choice is the eXtendible Output Functions (XOF) like SHAKE128/SHAKE256 of SHA-3.
Request output size from SHAKE128(or SHAKE256) equal to RSA modulus size, hash it then sign, that's it!
import hashlib
import rsa
(pubkey, privkey) = rsa.newkeys(2048)
FHD = hashlib.shake_128()
FHD.update(b'Message to Sign')
digestFDH = int.from_bytes(FHD.digest(255),byteorder='little')
#just m^d mod n
signed = rsa.core.decrypt_int(digestFDH,privkey.d,pubkey.n)
#just m^e mod n
if digestFDH == rsa.core.encrypt_int( signed ,pubkey.e,pubkey.n):
print("Verified")
else:
print("!!!Verification failed. Halt!!!")
The exact definition as in 1998 paper (not in quotes)
The signing and verifying algorithm have oracle access to a hash function $H_{FDH} : \{0, 1\}^∗ \to \mathbb{Z}^*_N$. Signature generation and verification are as follows :
$\operatorname{SignFDH}_{N,d}(M) $
$\quad y \leftarrow H_{FDH}(M)$
$\quad \text{return }y^d \bmod N$
$\operatorname{VerifyFDH}_{N,e}(M, x)$
$\quad y \leftarrow x^e \bmod N;$
$\quad y' \leftarrow H_{FDH}(M)$
$\quad\text{if }y = y' \text{ then return }1 \text{ else return } 0$
and note that, at least some of the elements of the $\mathbb{Z}_N^*$ cannot be output by a standard XOFs. The modulus is not an exact power of 2, so one needs to output one bit less than the modulus. The library that I've used for the sample implementation uses bytes for output size, so it cannot cover up to 8-bit.
Also, the all-zero output is excluded!.
- 49,797
- 12
- 123
- 211
Other answers had given a good overview of RSA signature schemes that has simple paddings that can be used securely.
I'd like to bring the attention of readers to the "RSA-KEM" encryption scheme (since this question is also tagged OAEP) specified in a standard-track RFC (which specifies its use in CMS the Cryptographic Message Syntax environments).
https://datatracker.ietf.org/doc/html/rfc5990#appendix-A
In essence, it's like a encryption counter part to the "full domain hash", where the key material encrypted by the RSA algorithm is "full-domain", that is, it's a value chosen, uniformly random, within the range of $[0,N)$ where $N$ is the public modulus.
It then use a key derivation function to derive a key, which is then used in a key-wrapping encryption.
- 10,640
- 2
- 27
- 64