diff --git a/tlslite/utils/compat.py b/tlslite/utils/compat.py index 7f019549..60f72c3a 100644 --- a/tlslite/utils/compat.py +++ b/tlslite/utils/compat.py @@ -5,6 +5,7 @@ import sys import os +import re import platform import math import binascii @@ -68,6 +69,10 @@ def formatExceptionTrace(e): """Return exception information formatted as string""" return str(e) + def remove_whitespace(text): + """Removes all whitespace from passed in string""" + return re.sub(r"\s+", "", text, flags=re.UNICODE) + else: # Python 2.6 requires strings instead of bytearrays in a couple places, # so we define this function so it does the conversion if needed. @@ -76,9 +81,18 @@ def formatExceptionTrace(e): if sys.version_info < (2, 7) or sys.version_info < (2, 7, 4) \ or platform.system() == 'Java': def compat26Str(x): return str(x) + + def remove_whitespace(text): + """Removes all whitespace from passed in string""" + return re.sub(r"\s+", "", text) + else: def compat26Str(x): return x + def remove_whitespace(text): + """Removes all whitespace from passed in string""" + return re.sub(r"\s+", "", text, flags=re.UNICODE) + def compatAscii2Bytes(val): """Convert ASCII string to bytes.""" return val diff --git a/tlslite/utils/constanttime.py b/tlslite/utils/constanttime.py index d4f5b1ce..950fca0c 100644 --- a/tlslite/utils/constanttime.py +++ b/tlslite/utils/constanttime.py @@ -23,6 +23,7 @@ def ct_lt_u32(val_a, val_b): return (val_a^((val_a^val_b)|(((val_a-val_b)&0xffffffff)^val_b)))>>31 + def ct_gt_u32(val_a, val_b): """ Return 1 if val_a > val_b, 0 otherwise. Constant time. @@ -35,6 +36,7 @@ def ct_gt_u32(val_a, val_b): """ return ct_lt_u32(val_b, val_a) + def ct_le_u32(val_a, val_b): """ Return 1 if val_a <= val_b, 0 otherwise. Constant time. @@ -47,14 +49,26 @@ def ct_le_u32(val_a, val_b): """ return 1 ^ ct_gt_u32(val_a, val_b) + def ct_lsb_prop_u8(val): - """Propagate LSB to all 8 bits of the returned byte. Constant time.""" + """Propagate LSB to all 8 bits of the returned int. Constant time.""" + val &= 0x01 + val |= val << 1 + val |= val << 2 + val |= val << 4 + return val + + +def ct_lsb_prop_u16(val): + """Propagate LSB to all 16 bits of the returned int. Constant time.""" val &= 0x01 val |= val << 1 val |= val << 2 val |= val << 4 + val |= val << 8 return val + def ct_isnonzero_u32(val): """ Returns 1 if val is != 0, 0 otherwise. Constant time. @@ -66,6 +80,7 @@ def ct_isnonzero_u32(val): val &= 0xffffffff return (val|(-val&0xffffffff)) >> 31 + def ct_neq_u32(val_a, val_b): """ Return 1 if val_a != val_b, 0 otherwise. Constant time. diff --git a/tlslite/utils/keyfactory.py b/tlslite/utils/keyfactory.py index 2e31fd91..dbacd5c7 100644 --- a/tlslite/utils/keyfactory.py +++ b/tlslite/utils/keyfactory.py @@ -107,21 +107,23 @@ def parsePEMKey(s, private=False, public=False, passwordCallback=None, def _parseKeyHelper(key, private, public): - if private: - if not key.hasPrivateKey(): - raise SyntaxError("Not a private key!") + if private and not key.hasPrivateKey(): + raise SyntaxError("Not a private key!") if public: return _createPublicKey(key) if private: - if hasattr(key, "d"): - return _createPrivateKey(key) - else: + if cryptomath.m2cryptoLoaded: + if type(key) == Python_RSAKey: + return _createPrivateKey(key) + assert type(key) in (OpenSSL_RSAKey, ), type(key) return key - + elif hasattr(key, "d"): + return _createPrivateKey(key) return key + def parseAsPublicKey(s): """Parse a PEM-formatted public key. diff --git a/tlslite/utils/openssl_rsakey.py b/tlslite/utils/openssl_rsakey.py index 71cb1033..e5377690 100644 --- a/tlslite/utils/openssl_rsakey.py +++ b/tlslite/utils/openssl_rsakey.py @@ -8,6 +8,7 @@ from .rsakey import * from .python_rsakey import Python_RSAKey from .compat import compatAscii2Bytes +import sys #copied from M2Crypto.util.py, so when we load the local copy of m2 #we can still use it @@ -65,12 +66,20 @@ def _rawPrivateKeyOp(self, m): c = bytesToNumber(bytearray(s)) return c + def _raw_private_key_op_bytes(self, message): + return bytearray(m2.rsa_private_encrypt(self.rsa, bytes(message), + m2.no_padding)) + def _rawPublicKeyOp(self, c): b = numberToByteArray(c, numBytes(self.n)) s = m2.rsa_public_decrypt(self.rsa, bytes(b), m2.no_padding) m = bytesToNumber(bytearray(s)) return m + def _raw_public_key_op_bytes(self, ciphertext): + return bytearray(m2.rsa_public_decrypt(self.rsa, bytes(ciphertext), + m2.no_padding)) + def acceptsPassword(self): return True def write(self, password=None): @@ -146,6 +155,13 @@ def f():pass key._hasPrivateKey = False else: raise SyntaxError() + if key._hasPrivateKey: + if sys.version_info < (3, 0): + b64_key = str(key.write()) + else: + b64_key = str(key.write(), "ascii") + py_key = Python_RSAKey.parsePEM(b64_key) + key.d = py_key.d return key finally: m2.bio_free(bio) diff --git a/tlslite/utils/python_key.py b/tlslite/utils/python_key.py new file mode 100644 index 00000000..356ff1bb --- /dev/null +++ b/tlslite/utils/python_key.py @@ -0,0 +1,130 @@ + + +from .python_rsakey import Python_RSAKey +from .pem import dePem, pemSniff +from .asn1parser import ASN1Parser +from .cryptomath import bytesToNumber + + +class Python_Key(object): + """ + Generic methods for parsing private keys from files. + + Handles both RSA and ECDSA keys, irrespective of file format. + """ + + @staticmethod + def parsePEM(s, passwordCallback=None): + """Parse a string containing a PEM-encoded .""" + + if pemSniff(s, "PRIVATE KEY"): + bytes = dePem(s, "PRIVATE KEY") + return Python_Key._parse_pkcs8(bytes) + elif pemSniff(s, "RSA PRIVATE KEY"): + bytes = dePem(s, "RSA PRIVATE KEY") + return Python_Key._parse_ssleay(bytes) + elif pemSniff(s, "DSA PRIVATE KEY"): + raise SyntaxError("DSA private key files unsupported") + elif pemSniff(s, "EC PRIVATE KEY"): + raise SyntaxError("ECDSA private key files unsupported") + elif pemSniff(s, "PUBLIC KEY"): + bytes = dePem(s, "PUBLIC KEY") + return Python_Key._parse_public_key(bytes) + else: + raise SyntaxError("Not a PEM private key file") + + @staticmethod + def _parse_public_key(bytes): + # public keys are encoded as the subject_public_key_info objects + spk_info = ASN1Parser(bytes) + + # first element of the SEQUENCE is the AlgorithmIdentifier + alg_id = spk_info.getChild(0) + + # AlgId has two elements, the OID of the algorithm and parameters + # parameters generally have to be NULL, with exception of RSA-PSS + + alg_oid = alg_id.getChild(0) + + if list(alg_oid.value) != [42, 134, 72, 134, 247, 13, 1, 1, 1]: + raise SyntaxError("Only RSA Public keys supported") + + subject_public_key = ASN1Parser( + ASN1Parser(spk_info.getChildBytes(1)).value[1:]) + + modulus = subject_public_key.getChild(0) + exponent = subject_public_key.getChild(1) + + n = bytesToNumber(modulus.value) + e = bytesToNumber(exponent.value) + + return Python_RSAKey(n, e) + + @staticmethod + def _parse_pkcs8(bytes): + parser = ASN1Parser(bytes) + + # first element in PrivateKeyInfo is an INTEGER + version = parser.getChild(0).value + if bytesToNumber(version) != 0: + raise SyntaxError("Unrecognized PKCS8 version") + + # second element in PrivateKeyInfo is a SEQUENCE of type + # AlgorithmIdentifier + alg_ident = parser.getChild(1) + seq_len = alg_ident.getChildCount() + # first item of AlgorithmIdentifier is an OBJECT (OID) + oid = alg_ident.getChild(0) + if list(oid.value) == [42, 134, 72, 134, 247, 13, 1, 1, 1]: + key_type = "rsa" + elif list(oid.value) == [42, 134, 72, 134, 247, 13, 1, 1, 10]: + key_type = "rsa-pss" + else: + raise SyntaxError("Unrecognized AlgorithmIdentifier: {0}" + .format(list(oid.value))) + # second item of AlgorithmIdentifier are parameters (defined by + # above algorithm) + if key_type == "rsa": + if seq_len != 2: + raise SyntaxError("Missing parameters for RSA algorithm ID") + parameters = alg_ident.getChild(1) + if parameters.value != bytearray(0): + raise SyntaxError("RSA parameters are not NULL") + else: # rsa-pss + pass # ignore parameters - don't apply restrictions + + if seq_len > 2: + raise SyntaxError("Invalid encoding of AlgorithmIdentifier") + + #Get the privateKey + private_key_parser = parser.getChild(2) + + #Adjust for OCTET STRING encapsulation + private_key_parser = ASN1Parser(private_key_parser.value) + + return Python_Key._parse_asn1_private_key(private_key_parser) + + @staticmethod + def _parse_ssleay(data): + """ + Parse binary structure of the old SSLeay file format used by OpenSSL. + + For RSA keys. + """ + private_key_parser = ASN1Parser(data) + return Python_Key._parse_asn1_private_key(private_key_parser) + + @staticmethod + def _parse_asn1_private_key(private_key_parser): + version = private_key_parser.getChild(0).value[0] + if version != 0: + raise SyntaxError("Unrecognized RSAPrivateKey version") + n = bytesToNumber(private_key_parser.getChild(1).value) + e = bytesToNumber(private_key_parser.getChild(2).value) + d = bytesToNumber(private_key_parser.getChild(3).value) + p = bytesToNumber(private_key_parser.getChild(4).value) + q = bytesToNumber(private_key_parser.getChild(5).value) + dP = bytesToNumber(private_key_parser.getChild(6).value) + dQ = bytesToNumber(private_key_parser.getChild(7).value) + qInv = bytesToNumber(private_key_parser.getChild(8).value) + return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv) diff --git a/tlslite/utils/python_rsakey.py b/tlslite/utils/python_rsakey.py index 91387493..100f7e84 100644 --- a/tlslite/utils/python_rsakey.py +++ b/tlslite/utils/python_rsakey.py @@ -86,79 +86,8 @@ def generate(bits): return key generate = staticmethod(generate) + @staticmethod def parsePEM(s, passwordCallback=None): """Parse a string containing a PEM-encoded .""" - - if pemSniff(s, "PRIVATE KEY"): - bytes = dePem(s, "PRIVATE KEY") - return Python_RSAKey._parsePKCS8(bytes) - elif pemSniff(s, "RSA PRIVATE KEY"): - bytes = dePem(s, "RSA PRIVATE KEY") - return Python_RSAKey._parseSSLeay(bytes) - else: - raise SyntaxError("Not a PEM private key file") - parsePEM = staticmethod(parsePEM) - - def _parsePKCS8(bytes): - p = ASN1Parser(bytes) - - # first element in PrivateKeyInfo is an INTEGER - version = p.getChild(0).value - if bytesToNumber(version) != 0: - raise SyntaxError("Unrecognized PKCS8 version") - - # second element in PrivateKeyInfo is a SEQUENCE of type - # AlgorithmIdentifier - algIdent = p.getChild(1) - seqLen = algIdent.getChildCount() - # first item of AlgorithmIdentifier is an OBJECT (OID) - oid = algIdent.getChild(0) - if list(oid.value) == [42, 134, 72, 134, 247, 13, 1, 1, 1]: - keyType = "rsa" - elif list(oid.value) == [42, 134, 72, 134, 247, 13, 1, 1, 10]: - keyType = "rsa-pss" - else: - raise SyntaxError("Unrecognized AlgorithmIdentifier: {0}" - .format(list(oid.value))) - # second item of AlgorithmIdentifier are parameters (defined by - # above algorithm) - if keyType == "rsa": - if seqLen != 2: - raise SyntaxError("Missing parameters for RSA algorithm ID") - parameters = algIdent.getChild(1) - if parameters.value != bytearray(0): - raise SyntaxError("RSA parameters are not NULL") - else: # rsa-pss - pass # ignore parameters - don't apply restrictions - - if seqLen > 2: - raise SyntaxError("Invalid encoding of AlgorithmIdentifier") - - #Get the privateKey - privateKeyP = p.getChild(2) - - #Adjust for OCTET STRING encapsulation - privateKeyP = ASN1Parser(privateKeyP.value) - - return Python_RSAKey._parseASN1PrivateKey(privateKeyP) - _parsePKCS8 = staticmethod(_parsePKCS8) - - def _parseSSLeay(bytes): - privateKeyP = ASN1Parser(bytes) - return Python_RSAKey._parseASN1PrivateKey(privateKeyP) - _parseSSLeay = staticmethod(_parseSSLeay) - - def _parseASN1PrivateKey(privateKeyP): - version = privateKeyP.getChild(0).value[0] - if version != 0: - raise SyntaxError("Unrecognized RSAPrivateKey version") - n = bytesToNumber(privateKeyP.getChild(1).value) - e = bytesToNumber(privateKeyP.getChild(2).value) - d = bytesToNumber(privateKeyP.getChild(3).value) - p = bytesToNumber(privateKeyP.getChild(4).value) - q = bytesToNumber(privateKeyP.getChild(5).value) - dP = bytesToNumber(privateKeyP.getChild(6).value) - dQ = bytesToNumber(privateKeyP.getChild(7).value) - qInv = bytesToNumber(privateKeyP.getChild(8).value) - return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv) - _parseASN1PrivateKey = staticmethod(_parseASN1PrivateKey) + from .python_key import Python_Key + return Python_Key.parsePEM(s, passwordCallback) diff --git a/tlslite/utils/rsakey.py b/tlslite/utils/rsakey.py index 5c8c675b..e2bb8816 100644 --- a/tlslite/utils/rsakey.py +++ b/tlslite/utils/rsakey.py @@ -7,6 +7,8 @@ from . import tlshashlib as hashlib from ..errors import MaskTooLongError, MessageTooLongError, EncodingError, \ InvalidSignature, UnknownRSAType +from .constanttime import ct_isnonzero_u32, ct_neq_u32, ct_lsb_prop_u8, \ + ct_lsb_prop_u16, ct_lt_u32 class RSAKey(object): @@ -34,6 +36,7 @@ def __init__(self, n=0, e=0): :type e: int :param e: RSA public exponent. """ + self._key_hash = None raise NotImplementedError() def __len__(self): @@ -187,12 +190,11 @@ def RSASSA_PSS_sign(self, mHash, hAlg, sLen=0): :type sLen: int :param sLen: length of salt""" EM = self.EMSA_PSS_encode(mHash, numBits(self.n) - 1, hAlg, sLen) - m = bytesToNumber(EM) - if m >= self.n: + try: + ret = self._raw_private_key_op_bytes(EM) + except ValueError: raise MessageTooLongError("Encode output too long") - s = self._rawPrivateKeyOp(m) - S = numberToByteArray(s, numBytes(self.n)) - return S + return ret def EMSA_PSS_verify(self, mHash, EM, emBits, hAlg, sLen=0): """Verify signature in passed in encoded message @@ -264,11 +266,10 @@ def RSASSA_PSS_verify(self, mHash, S, hAlg, sLen=0): :type sLen: int :param sLen: Length of salt """ - if len(bytearray(S)) != len(numberToByteArray(self.n)): + try: + EM = self._raw_public_key_op_bytes(S) + except ValueError: raise InvalidSignature("Invalid signature") - s = bytesToNumber(S) - m = self._rawPublicKeyOp(s) - EM = numberToByteArray(m, divceil(numBits(self.n) - 1, 8)) result = self.EMSA_PSS_verify(mHash, EM, numBits(self.n) - 1, hAlg, sLen) if result: @@ -281,12 +282,7 @@ def _raw_pkcs1_sign(self, bytes): if not self.hasPrivateKey(): raise AssertionError() paddedBytes = self._addPKCS1Padding(bytes, 1) - m = bytesToNumber(paddedBytes) - if m >= self.n: - raise ValueError() - c = self._rawPrivateKeyOp(m) - sigBytes = numberToByteArray(c, numBytes(self.n)) - return sigBytes + return self._raw_private_key_op_bytes(paddedBytes) def sign(self, bytes, padding='pkcs1', hashAlg=None, saltLen=None): """Sign the passed-in bytes. @@ -326,14 +322,11 @@ def sign(self, bytes, padding='pkcs1', hashAlg=None, saltLen=None): def _raw_pkcs1_verify(self, sigBytes, bytes): """Perform verification operation on raw PKCS#1 padded signature""" - if len(sigBytes) != numBytes(self.n): + try: + checkBytes = self._raw_public_key_op_bytes(sigBytes) + except ValueError: return False paddedBytes = self._addPKCS1Padding(bytes, 1) - c = bytesToNumber(sigBytes) - if c >= self.n: - return False - m = self._rawPublicKeyOp(c) - checkBytes = numberToByteArray(m, numBytes(self.n)) return checkBytes == paddedBytes def verify(self, sigBytes, bytes, padding='pkcs1', hashAlg=None, @@ -384,45 +377,176 @@ def encrypt(self, bytes): :returns: A PKCS1 encryption of the passed-in data. """ paddedBytes = self._addPKCS1Padding(bytes, 2) - m = bytesToNumber(paddedBytes) - if m >= self.n: - raise ValueError() - c = self._rawPublicKeyOp(m) - encBytes = numberToByteArray(c, numBytes(self.n)) - return encBytes + return self._raw_public_key_op_bytes(paddedBytes) + + def _dec_prf(self, key, label, out_len): + """PRF for deterministic implicit rejection in the RSA decryption. + + :param bytes key: key to use for derivation + :param bytes label: name of the keystream generated + :param int out_len: length of output, in bits + :rtype: bytes + :returns: a random bytestring + """ + out = bytearray() + + if out_len % 8 != 0: + raise ValueError("only multiples of 8 supported as output size") + + iterator = 0 + while len(out) < out_len // 8: + out += secureHMAC( + key, + numberToByteArray(iterator, 2) + label + + numberToByteArray(out_len, 2), + "sha256") + iterator += 1 + + return out[:out_len//8] def decrypt(self, encBytes): """Decrypt the passed-in bytes. This requires the key to have a private component. It performs - PKCS1 decryption of the passed-in data. + PKCS#1 v1.5 decryption operation of the passed-in data. + + Note: as a workaround against Bleichenbacher-like attacks, it will + return a deterministically selected random message in case the padding + checks failed. It returns an error (None) only in case the ciphertext + is of incorrect length or encodes an integer bigger than the modulus + of the key (i.e. it's publically invalid). :type encBytes: bytearray :param encBytes: The value which will be decrypted. :rtype: bytearray or None - :returns: A PKCS1 decryption of the passed-in data or None if - the data is not properly formatted. + :returns: A PKCS#1 v1.5 decryption of the passed-in data or None if + the provided data is not properly formatted. Note: encrypting + an empty string is correct, so it may return an empty bytearray + for some ciphertexts. """ if not self.hasPrivateKey(): raise AssertionError() - if len(encBytes) != numBytes(self.n): - return None - c = bytesToNumber(encBytes) - if c >= self.n: + try: + dec_bytes = self._raw_private_key_op_bytes(encBytes) + except ValueError: + # _raw_private_key_op_bytes fails only when encBytes >= self.n, + # or when len(encBytes) != numBytes(self.n) and that's public + # information, so we don't have to handle it + # in sidechannel secure way return None - m = self._rawPrivateKeyOp(c) - decBytes = numberToByteArray(m, numBytes(self.n)) - #Check first two bytes - if decBytes[0] != 0 or decBytes[1] != 2: - return None - #Scan through for zero separator - for x in range(1, len(decBytes)-1): - if decBytes[x]== 0: - break - else: - return None - return decBytes[x+1:] #Return everything after the separator + + ################### + # here be dragons # + ################### + # While the code is written as-if it was side-channel secure, in + # practice, because of cPython implementation details IT IS NOT + # see: + # https://securitypitfalls.wordpress.com/2018/08/03/constant-time-compare-in-python/ + + n = self.n + + # maximum length we can return is reduced by the mandatory prefix: + # (0x00 0x02), 8 bytes of padding, so this is the position of the + # null separator byte, as counted from the last position + max_sep_offset = numBytes(n) - 10 + + # the private exponent (d) doesn't change so `_key_hash` doesn't + # change, calculate it only once + if not hasattr(self, '_key_hash') or not self._key_hash: + self._key_hash = secureHash(numberToByteArray(self.d, numBytes(n)), + "sha256") + + kdk = secureHMAC(self._key_hash, encBytes, "sha256") + + # we need 128 2-byte numbers, encoded as the number of bits + length_randoms = self._dec_prf(kdk, b"length", 128 * 2 * 8) + + message_random = self._dec_prf(kdk, b"message", numBytes(n) * 8) + + # select the last length that's not too large to return + synth_length = 0 + length_rand_iter = iter(length_randoms) + length_mask = (1 << numBits(max_sep_offset)) - 1 + for high, low in zip(length_rand_iter, length_rand_iter): + # interpret the two bytes from the PRF output as 16-bit big-endian + # integer + len_candidate = (high << 8) + low + len_candidate &= length_mask + # equivalent to: + # if len_candidate < max_sep_offset: + # synth_length = len_candidate + mask = ct_lt_u32(len_candidate, max_sep_offset) + mask = ct_lsb_prop_u16(mask) + synth_length = synth_length & (0xffff ^ mask) \ + | len_candidate & mask + + synth_msg_start = numBytes(n) - synth_length + + error_detected = 0 + + # enumerate over all decrypted bytes + em_bytes = enumerate(dec_bytes) + # first check if first two bytes specify PKCS#1 v1.5 encryption padding + _, val = next(em_bytes) + error_detected |= ct_isnonzero_u32(val) + _, val = next(em_bytes) + error_detected |= ct_neq_u32(val, 0x02) + # then look for for the null separator byte among the padding bytes + # but inspect all decrypted bytes, even if we already find the + # separator earlier + msg_start = 0 + for pos, val in em_bytes: + # padding must be at least 8 bytes long, fail if any of the first + # 8 bytes of it are zero + # equivalent to: + # if pos < 10 and not val: + # error_detected = 0x01 + error_detected |= ct_lt_u32(pos, 10) & (1 ^ ct_isnonzero_u32(val)) + + # update the msg_start only once; when it's 0 + # (pos+1) because we want to skip the null separator + # equivalent to: + # if pos >= 10 and not msg_start and not val: + # msg_start = pos+1 + mask = (1 ^ ct_lt_u32(pos, 10)) & (1 ^ ct_isnonzero_u32(val)) \ + & (1 ^ ct_isnonzero_u32(msg_start)) + mask = ct_lsb_prop_u16(mask) + msg_start = msg_start & (0xffff ^ mask) | (pos+1) & mask + + # if separator wasn't found, it's an error + # equivalent to: + # if not msg_start: + # error_detected = 0x01 + error_detected |= 1 ^ ct_isnonzero_u32(msg_start) + + # equivalent to: + # if error_detected: + # ret_msg_start = synth_msg_start + # else: + # ret_msg_start = msg_start + mask = ct_lsb_prop_u16(error_detected) + ret_msg_start = msg_start & (0xffff ^ mask) | synth_msg_start & mask + + # as at this point the length doesn't leak the information if the + # padding was correct or not, we don't have to worry about the + # length of the returned value (and thus the size of the buffer we + # pass to the caller); but we still need to read both buffers + # to ensure that the memory access patern is preserved (that both + # buffers are accessed, not just the one we return) + + # equivalent to: + # if error_detected: + # return message_random[ret_msg_start:] + # else: + # return dec_bytes[ret_msg_start:] + mask = ct_lsb_prop_u8(error_detected) + not_mask = 0xff ^ mask + ret = bytearray( + x & not_mask | y & mask for x, y in + zip(dec_bytes[ret_msg_start:], message_random[ret_msg_start:])) + + return ret def _rawPrivateKeyOp(self, m): raise NotImplementedError() @@ -430,6 +554,26 @@ def _rawPrivateKeyOp(self, m): def _rawPublicKeyOp(self, c): raise NotImplementedError() + def _raw_private_key_op_bytes(self, message): + n = self.n + if len(message) != numBytes(n): + raise ValueError("Message has incorrect length for the key size") + m_int = bytesToNumber(message) + if m_int >= n: + raise ValueError("Provided message value exceeds modulus") + dec_int = self._rawPrivateKeyOp(m_int) + return numberToByteArray(dec_int, numBytes(n)) + + def _raw_public_key_op_bytes(self, ciphertext): + n = self.n + if len(ciphertext) != numBytes(n): + raise ValueError("Message has incorrect length for the key size") + c_int = bytesToNumber(ciphertext) + if c_int >= n: + raise ValueError("Provided message value exceeds modulus") + enc_int = self._rawPublicKeyOp(c_int) + return numberToByteArray(enc_int, numBytes(n)) + def acceptsPassword(self): """Return True if the write() method accepts a password for use in encrypting the private key. diff --git a/unit_tests/test_tlslite_utils_constanttime.py b/unit_tests/test_tlslite_utils_constanttime.py index 0a6446d0..2c04da5c 100644 --- a/unit_tests/test_tlslite_utils_constanttime.py +++ b/unit_tests/test_tlslite_utils_constanttime.py @@ -11,7 +11,7 @@ from tlslite.utils.constanttime import ct_lt_u32, ct_gt_u32, ct_le_u32, \ ct_lsb_prop_u8, ct_isnonzero_u32, ct_neq_u32, ct_eq_u32, \ - ct_check_cbc_mac_and_pad, ct_compare_digest + ct_check_cbc_mac_and_pad, ct_compare_digest, ct_lsb_prop_u16 from hypothesis import given, example import hypothesis.strategies as st @@ -80,6 +80,14 @@ def test_ct_lsb_prop_u8(self, i): self.assertEqual(((i & 0x1) == 1), (ct_lsb_prop_u8(i) == 0xff)) self.assertEqual(((i & 0x1) == 0), (ct_lsb_prop_u8(i) == 0x00)) + @given(i=st.integers(0, 2**16-1)) + @example(i=0) + @example(i=255) + @example(i=2**16-1) + def test_ct_lsb_prop_u16(self, i): + self.assertEqual(((i & 0x1) == 1), (ct_lsb_prop_u16(i) == 0xffff)) + self.assertEqual(((i & 0x1) == 0), (ct_lsb_prop_u16(i) == 0x0000)) + @given(i=st.integers(0,2**32 - 1)) @example(i=0) def test_ct_isnonzero_u32(self, i): diff --git a/unit_tests/test_tlslite_utils_rsakey.py b/unit_tests/test_tlslite_utils_rsakey.py index 73331fa7..3fbcf62a 100644 --- a/unit_tests/test_tlslite_utils_rsakey.py +++ b/unit_tests/test_tlslite_utils_rsakey.py @@ -13,6 +13,8 @@ from tlslite.utils.python_rsakey import Python_RSAKey from tlslite.utils.cryptomath import * from tlslite.errors import * +from tlslite.utils.keyfactory import parsePEMKey +from tlslite.utils.compat import a2b_hex, remove_whitespace try: import mock from mock import call @@ -1642,3 +1644,1235 @@ def test_addPKCS1Prefix(self): self.assertEqual(RSAKey.addPKCS1Prefix(data, 'sha1'), bytearray( b'0!0\t\x06\x05+\x0e\x03\x02\x1a\x05\x00\x04\x14' + b' sha-1 hash of data ')) + + +class TestRSADecrypt(unittest.TestCase): + @classmethod + def setUpClass(cls): + priv_key = """ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAyMyDlxQJjaVsqiNkD5PciZfBY3KWj8Gwxt9RE8HJTosh5IrS +KX5lQZARtObY9ec7G3iyV0ADIdHva2AtTsjOjRQclJBetK0wZjmkkgZTS25/JgdC +Ppff/RM8iNchOZ3vvH6WzNy9fzquH+iScSv7SSmBfVEWZkQKH6y3ogj16hZZEK3Y +o/LUlyAjYMy2MgJPDQcWnBkY8xb3lLFDrvVOyHUipMApePlomYC/+/ZJwwfoGBm/ ++IQJY41IvZS+FStZ/2SfoL1inQ/6GBPDq/S1a9PC6lRl3/oUWJKSqdiiStJr5+4F +EHQbY4LUPIPVv6QKRmE9BivkRVF9vK8MtOGnaQIDAQABAoIBABRVAQ4PLVh2Y6Zm +pv8czbvw7dgQBkbQKgI5IpCJksStOeVWWSlybvZQjDpxFY7wtv91HTnQdYC7LS8G +MhBELQYD/1DbvXs1/iybsZpHoa+FpMJJAeAsqLWLeRmyDt8yqs+/Ua20vEthubfp +aMqk1XD3DvGNgGMiiJPkfUOe/KeTJZvPLNEIo9hojN8HjnrHmZafIznSwfUiuWlo +RimpM7quwmgWJeq4T05W9ER+nYj7mhmc9xAj4OJXsURBszyE07xnyoAx0mEmGBA6 +egpAhEJi912IkM1hblH5A1SI/W4Jnej/bWWk/xGCVIB8n1jS+7qLoVHcjGi+NJyX +eiBOBMECgYEA+PWta6gokxvqRZuKP23AQdI0gkCcJXHpY/MfdIYColY3GziD7UWe +z5cFJkWe3RbgVSL1pF2UdRsuwtrycsf4gWpSwA0YCAFxY02omdeXMiL1G5N2MFSG +lqn32MJKWUl8HvzUVc+5fuhtK200lyszL9owPwSZm062tcwLsz53Yd0CgYEAznou +O0mpC5YzChLcaCvfvfuujdbcA7YUeu+9V1dD8PbaTYYjUGG3Gv2crS00Al5WrIaw +93Q+s14ay8ojeJVCRGW3Bu0iF15XGMjHC2cD6o9rUQ+UW+SOWja7PDyRcytYnfwF +1y2AkDGURSvaITSGR+xylD8RqEbmL66+jrU2sP0CgYB2/hXxiuI5zfHfa0RcpLxr +uWjXiMIZM6T13NKAAz1nEgYswIpt8gTB+9C+RjB0Q+bdSmRWN1Qp1OA4yiVvrxyb +3pHGsXt2+BmV+RxIy768e/DjSUwINZ5OjNalh9e5bWIh/X4PtcVXXwgu5XdpeYBx +sru0oyI4FRtHMUu2VHkDEQKBgQCZiEiwVUmaEAnLx9KUs2sf/fICDm5zZAU+lN4a +AA3JNAWH9+JydvaM32CNdTtjN3sDtvQITSwCfEs4lgpiM7qe2XOLdvEOp1vkVgeL +9wH2fMaz8/3BhuZDNsdrNy6AkQ7ICwrcwj0C+5rhBIaigkgHW06n5W3fzziC5FFW +FHGikQKBgGQ790ZCn32DZnoGUwITR++/wF5jUfghqd67YODszeUAWtnp7DHlWPfp +LCkyjnRWnXzvfHTKvCs1XtQBoaCRS048uwZITlgZYFEWntFMqi76bqBE4FTSYUTM +FinFUBBVigThM/RLfCRNrCW/kTxXuJDuSfVIJZzWNAT+9oWdz5da +-----END RSA PRIVATE KEY----- +""" + cls.priv_key = parsePEMKey(priv_key, private=True) + + pub_key = """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyMyDlxQJjaVsqiNkD5Pc +iZfBY3KWj8Gwxt9RE8HJTosh5IrSKX5lQZARtObY9ec7G3iyV0ADIdHva2AtTsjO +jRQclJBetK0wZjmkkgZTS25/JgdCPpff/RM8iNchOZ3vvH6WzNy9fzquH+iScSv7 +SSmBfVEWZkQKH6y3ogj16hZZEK3Yo/LUlyAjYMy2MgJPDQcWnBkY8xb3lLFDrvVO +yHUipMApePlomYC/+/ZJwwfoGBm/+IQJY41IvZS+FStZ/2SfoL1inQ/6GBPDq/S1 +a9PC6lRl3/oUWJKSqdiiStJr5+4FEHQbY4LUPIPVv6QKRmE9BivkRVF9vK8MtOGn +aQIDAQAB +-----END PUBLIC KEY----- +""" + cls.pub_key = parsePEMKey(pub_key, public=True) + + def test_sanity(self): + self.assertIsNotNone(self.priv_key) + self.assertIsNotNone(self.pub_key) + + self.assertEqual( + self.priv_key.d, + bytesToNumber(a2b_hex( + "1455010e0f2d587663a666a6ff1ccdbbf0edd8100646d02a023922908992" + "c4ad39e5565929726ef6508c3a71158ef0b6ff751d39d07580bb2d2f0632" + "10442d0603ff50dbbd7b35fe2c9bb19a47a1af85a4c24901e02ca8b58b79" + "19b20edf32aacfbf51adb4bc4b61b9b7e968caa4d570f70ef18d80632288" + "93e47d439efca793259bcf2cd108a3d8688cdf078e7ac799969f2339d2c1" + "f522b969684629a933baaec2681625eab84f4e56f4447e9d88fb9a199cf7" + "1023e0e257b14441b33c84d3bc67ca8031d2612618103a7a0a40844262f7" + "5d8890cd616e51f9035488fd6e099de8ff6d65a4ff118254807c9f58d2fb" + "ba8ba151dc8c68be349c977a204e04c1"))) + + def test_simple_encypt_decrypt(self): + # just verify that decrypting encrypted message gives the expected + # message back + self.assertEqual( + self.priv_key.decrypt(self.pub_key.encrypt(b'message')), + b'message') + + def test_decryption(self): + # a random positive test case + ciphertext = a2b_hex(remove_whitespace(""" +8bfe264e85d3bdeaa6b8851b8e3b956ee3d226fd3f69063a86880173a273d9f283b2eebdd1ed +35f7e02d91c571981b6737d5320bd8396b0f3ad5b019daec1b0aab3cbbc026395f4fd14f1367 +3f2dfc81f9b660ec26ac381e6db3299b4e460b43fab9955df2b3cfaa20e900e19c856238fd37 +1899c2bf2ce8c868b76754e5db3b036533fd603746be13c10d4e3e6022ebc905d20c2a7f32b2 +15a4cd53b3f44ca1c327d2c2b651145821c08396c89071f665349c25e44d2733cd9305985cee +f6430c3cf57af5fa224089221218fa34737c79c446d28a94c41c96e4e92ac53fbcf384dea841 +9ea089f8784445a492c812eb0d409467f75afd7d4d1078886205a066""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + msg = self.priv_key.decrypt(ciphertext) + self.assertEqual(msg, b'lorem ipsum dolor sit amet') + + def test_invalid_decrypting_to_empty(self): + ciphertext = a2b_hex(remove_whitespace(""" +20aaa8adbbc593a924ba1c5c7990b5c2242ae4b99d0fe636a19a4cf754edbcee774e472fe028 +160ed42634f8864900cb514006da642cae6ae8c7d087caebcfa6dad1551301e130344989a1d4 +62d4164505f6393933450c67bc6d39d8f5160907cabc251b737925a1cf21e5c6aa5781b7769f +6a2a583d97cce008c0f8b6add5f0b2bd80bee60237aa39bb20719fe75749f4bc4e42466ef5a8 +61ae3a92395c7d858d430bfe38040f445ea93fa2958b503539800ffa5ce5f8cf51fa8171a91f +36cb4f4575e8de6b4d3f096ee140b938fd2f50ee13f0d050222e2a72b0a3069ff3a6738e82c8 +7090caa5aed4fcbe882c49646aa250b98f12f83c8d528113614a29e7""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertNotEqual(dec[-1:], b'\x00') + + msg = self.priv_key.decrypt(ciphertext) + self.assertEqual(msg, b'') + + def test_invalid_decrypting_to_max_length(self): + # the last value from PRF is 245, which is exactly the max we + # can return + ciphertext = a2b_hex(remove_whitespace(""" +48cceab10f39a4db32f60074feea473cbcdb7accf92e150417f76b44756b190e843e79ec12aa +85083a21f5437e7bad0a60482e601198f9d86923239c8786ee728285afd0937f7dde12717f28 +389843d7375912b07b991f4fdb0190fced8ba665314367e8c5f9d2981d0f5128feeb46cb50fc +237e64438a86df198dd0209364ae3a842d77532b66b7ef263b83b1541ed671b120dfd660462e +2107a4ee7b964e734a7bd68d90dda61770658a3c242948532da32648687e0318286473f675b4 +12d6468f013f14d760a358dfcad3cda2afeec5e268a37d250c37f722f468a70dfd92d7294c3c +1ee1e7f8843b7d16f9f37ef35748c3ae93aa155cdcdfeb4e78567303""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual( + dec[0:11], + b'\x78\x05\x5c\xc0\xd7\x02\xfe\xd7\x6a\xbe\x53') + + plaintext = a2b_hex(remove_whitespace(""" +22d850137b9eebe092b24f602dc5bb7918c16bd89ddbf20467b119d205f9c2e4bd7d2592cf1e +532106e0f33557565923c73a02d4f09c0c22bea89148183e60317f7028b3aa1f261f91c97939 +3101d7e15f4067e63979b32751658ef769610fe97cf9cef3278b3117d384051c3b1d82c251c2 +305418c8f6840530e631aad63e70e20e025bcd8efb54c92ec6d3b106a2f8e64eeff7d38495b0 +fc50c97138af4b1c0a67a1c4e27b077b8439332edfa8608dfeae653cd6a628ac550395f7e743 +90e42c11682234870925eeaa1fa71b76cf1f2ee3bda69f6717033ff8b7c95c9799e7a3bea5e7 +e4a1c359772fb6b1c6e6c516661dfe30c3""")) + self.assertEqual(len(plaintext), 245) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_invalid_decrypting_to_length_second_to_last_from_prf(self): + # the last value from the PRF is 246, which is longer than the max + # allowed length: 245, so it needs to select second to last: 2 + ciphertext = a2b_hex(remove_whitespace(""" +1439e08c3f84c1a7fec74ce07614b20e01f6fa4e8c2a6cffdc3520d8889e5d9a950c6425798f +85d4be38d300ea5695f13ecd4cb389d1ff5b82484b494d6280ab7fa78e645933981cb934cce8 +bfcd114cc0e6811eefa47aae20af638a1cd163d2d3366186d0a07df0c81f6c9f3171cf356147 +2e98a6006bf75ddb457bed036dcce199369de7d94ef2c68e8467ee0604eea2b3009479162a78 +91ba5c40cab17f49e1c438cb6eaea4f76ce23cce0e483ff0e96fa790ea15be67671814342d0a +23f4a20262b6182e72f3a67cd289711503c85516a9ed225422f98b116f1ab080a80abd6f0216 +df88d8cfd67c139243be8dd78502a7aaf6bc99d7da71bcdf627e7354""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertEqual(dec[-3:], b'\xd1\x90\x17') + + plaintext = a2b_hex(remove_whitespace("0f9b")) + + self.assertEqual(len(plaintext), 2) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_invalid_decrypting_to_length_third_to_last_from_prf(self): + # the last three numbers from prf are: 2, 247, 255, so we need to + # pick 2, the third one from the end + ciphertext = a2b_hex(remove_whitespace(""" +1690ebcceece2ce024f382e467cf8510e74514120937978576caf684d4a02ad569e8d76cbe36 +5a060e00779de2f0865ccf0d923de3b4783a4e2c74f422e2f326086c390b658ba47f31ab013a +a80f468c71256e5fa5679b24e83cd82c3d1e05e398208155de2212993cd2b8bab6987cf4cc12 +93f19909219439d74127545e9ed8a706961b8ee2119f6bfacafbef91b75a789ba65b8b833bc6 +149cf49b5c4d2c6359f62808659ba6541e1cd24bf7f7410486b5103f6c0ea29334ea6f4975b1 +7387474fe920710ea61568d7b7c0a7916acf21665ad5a31c4eabcde44f8fb6120d8457afa1f3 +c85d517cda364af620113ae5a3c52a048821731922737307f77a1081""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertEqual(dec[-3:], b'\xee\xaf\xde') + + plaintext = a2b_hex(remove_whitespace("4f02")) + + self.assertEqual(len(plaintext), 2) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_11_byte_long(self): + # ciphertext that generates a fake 11 byte plaintext, but decrypts + # to real 11 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +6213634593332c485cef783ea2846e3d6e8b0e005cd8293eaebbaa5079712fd681579bdfbbda +138ae4d9d952917a03c92398ec0cb2bb0c6b5a8d55061fed0d0d8d72473563152648cfe640b3 +35dc95331c21cb133a91790fa93ae44497c128708970d2beeb77e8721b061b1c44034143734a +77be8220877415a6dba073c3871605380542a9f25252a4babe8331cdd53cf828423f3cc70b56 +0624d0581fb126b2ed4f4ed358f0eb8065cf176399ac1a846a31055f9ae8c9c24a1ba050bc20 +842125bc1753158f8065f3adb9cc16bfdf83816bdf38b624f12022c5a6fbfe29bc91542be8c0 +208a770bcd677dc597f5557dc2ce28a11bf3e3857f158717a33f6592""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + plaintext = b'lorem ipsum' + + self.assertEqual(len(plaintext), 11) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_11_byte_long_with_null_padded_ciphertext(self): + # ciphertext that starts with a null byte, decrypts to real 11 byte + # long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +00a2e8f114ea8d05d12dc843e3cc3b2edc8229ff2a028bda29ba9d55e3cd02911902fef1f42a +075bf05e8016e8567213d6f260fa49e360779dd81aeea3e04c2cb567e0d72b98bf754014561b +7511e083d20e0bfb9cd23f8a0d3c88900c49d2fcd5843ff0765607b2026f28202a87aa94678a +ed22a0c20724541394cd8f44e373eba1d2bae98f516c1e2ba3d86852d064f856b1daf24795e7 +67a2b90396e50743e3150664afab131fe40ea405dcf572dd1079af1d3f0392ccadcca0a12740 +dbb213b925ca2a06b1bc1383e83a658c82ba2e7427342379084d5f66b544579f07664cb26edd +4f10fd913fdbc0de05ef887d4d1ec1ac95652397ea7fd4e4759fda8b""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + plaintext = b'lorem ipsum' + + self.assertEqual(len(plaintext), 11) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_11_byte_long_with_double_null_padded_ciphertext(self): + # ciphertext that starts with two null bytes, decrypts to real 11 byte + # long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +00001f71879b426127f7dead621f7380a7098cf7d22173aa27991b143c46d53383c209bd0c9c +00d84078037e715f6b98c65005a77120070522ede51d472c87ef94b94ead4c5428ee108a3455 +61658301911ec5a8f7dd43ed4a3957fd29fb02a3529bf63f8040d3953490939bd8f78b2a3404 +b6fb5ff70a4bfdaac5c541d6bcce49c9778cc390be24cbef1d1eca7e870457241d3ff72ca44f +9f56bdf31a890fa5eb3a9107b603ccc9d06a5dd911a664c82b6abd4fe036f8db8d5a070c2d86 +386ae18d97adc1847640c211d91ff5c3387574a26f8ef27ca7f48d2dd1f0c7f14b81cc9d33ee +6853031d3ecf10a914ffd90947909c8011fd30249219348ebff76bfc""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + plaintext = b'lorem ipsum' + + self.assertEqual(len(plaintext), 11) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_11_byte_long_with_zero_generated_length(self): + # valid ciphertext that generates a zero length fake plaintext + ciphertext = a2b_hex(remove_whitespace(""" +b5e49308f6e9590014ffaffc5b8560755739dd501f1d4e9227a7d291408cf4b753f292322ff8 +bead613bf2caa181b221bc38caf6392deafb28eb21ad60930841ed02fd6225cc9c463409adbe +7d8f32440212fbe3881c51375bb09565efb22e62b071472fb38676e5b4e23a0617db5d14d935 +19ac0007a30a9c822eb31c38b57fcb1be29608fcf1ca2abdcaf5d5752bbc2b5ac7dba5afcff4 +a5641da360dd01f7112539b1ed46cdb550a3b1006559b9fe1891030ec80f0727c42401ddd6cb +b5e3c80f312df6ec89394c5a7118f573105e7ab00fe57833c126141b50a935224842addfb479 +f75160659ba28877b512bb9a93084ad8bec540f92640f63a11a010e0""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + plaintext = b'lorem ipsum' + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_11_byte_long_with_245_generated_length(self): + # valid ciphertext that generates a 245 byte long fake plaintext + ciphertext = a2b_hex(remove_whitespace(""" +1ea0b50ca65203d0a09280d39704b24fe6e47800189db5033f202761a78bafb270c5e25abd1f +7ecc6e7abc4f26d1b0cd9b8c648d529416ee64ccbdd7aa72a771d0353262b543f0e436076f40 +a1095f5c7dfd10dcf0059ccb30e92dfa5e0156618215f1c3ff3aa997a9d999e506924f5289e3 +ac72e5e2086cc7b499d71583ed561028671155db4005bee01800a7cdbdae781dd32199b8914b +5d4011dd6ff11cd26d46aad54934d293b0bc403dd211bf13b5a5c6836a5e769930f437ffd863 +4fb7371776f4bc88fa6c271d8aa6013df89ae6470154497c4ac861be2a1c65ebffec139bf7aa +ba3a81c7c5cdd84da9af5d3edfb957848074686b5837ecbcb6a41c50""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + plaintext = b"lorem ipsum" + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long(self): + # a random ciphertext that generates a fake 11 byte plaintext + # and fails padding check + ciphertext = a2b_hex(remove_whitespace(""" +5f02f4b1f46935c742ebe62b6f05aa0a3286aab91a49b34780adde6410ab46f7386e05748331 +864ac98e1da63686e4babe3a19ed40a7f5ceefb89179596aab07ab1015e03b8f825084dab028 +b6731288f2e511a4b314b6ea3997d2e8fe2825cef8897cbbdfb6c939d441d6e04948414bb69e +682927ef8576c9a7090d4aad0e74c520d6d5ce63a154720f00b76de8cc550b1aa14f016d63a7 +b6d6eaa1f7dbe9e50200d3159b3d099c900116bf4eba3b94204f18b1317b07529751abf64a26 +b0a0bf1c8ce757333b3d673211b67cc0653f2fe2620d57c8b6ee574a0323a167eab1106d9bc7 +fd90d415be5f1e9891a0e6c709f4fc0404e8226f8477b4e939b36eb2""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertNotEqual(dec[-12:-11], b'\x00') + + plaintext = a2b_hex(remove_whitespace("af9ac70191c92413cb9f2d")) + + self.assertEqual(len(plaintext), 11) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'lorem ipsum') + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long_wrong_version_byte(self): + # an otherwise correct plaintext, but with wrong first byte + # (0x01 instead of 0x00), generates a random 11 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +9b2ec9c0c917c98f1ad3d0119aec6be51ae3106e9af1914d48600ab6a2c0c0c8ae02a2dc3039 +906ff3aac904af32ec798fd65f3ad1afa2e69400e7c1de81f5728f3b3291f38263bc7a90a056 +3e43ce7a0d4ee9c0d8a716621ca5d3d081188769ce1b131af7d35b13dea99153579c86db31fe +07d5a2c14d621b77854e48a8df41b5798563af489a291e417b6a334c63222627376118c02c53 +b6e86310f728734ffc86ef9d7c8bf56c0c841b24b82b59f51aee4526ba1c4268506d301e4ebc +498c6aebb6fd5258c876bf900bac8ca4d309dd522f6a6343599a8bc3760f422c10c72d0ad527 +ce4af1874124ace3d99bb74db8d69d2528db22c3a37644640f95c05f""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x01\x02') + self.assertEqual(dec[-12:], b'\x00lorem ipsum') + + plaintext = a2b_hex(remove_whitespace("a1f8c9255c35cfba403ccc")) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'lorem ipsum') + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long_wrong_type_byte(self): + # an otherwise correct plaintext, but with wrong second byte + # (0x01 instead of 0x02), generates a random 11 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +782c2b59a21a511243820acedd567c136f6d3090c115232a82a5efb0b178285f55b5ec2d2bac +96bf00d6592ea7cdc3341610c8fb07e527e5e2d20cfaf2c7f23e375431f45e998929a02f25fd +95354c33838090bca838502259e92d86d568bc2cdb132fab2a399593ca60a015dc2bb1afcd64 +fef8a3834e17e5358d822980dc446e845b3ab4702b1ee41fe5db716d92348d5091c15d35a110 +555a35deb4650a5a1d2c98025d42d4544f8b32aa6a5e02dc02deaed9a7313b73b49b0d4772a3 +768b0ea0db5846ace6569cae677bf67fb0acf3c255dc01ec8400c963b6e49b1067728b4e563d +7e1e1515664347b92ee64db7efb5452357a02fff7fcb7437abc2e579""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x00\x01') + self.assertEqual(dec[-12:], b'\x00lorem ipsum') + + plaintext = a2b_hex(remove_whitespace("e6d700309ca0ed62452254")) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'lorem ipsum') + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long_null_type_byte(self): + # an otherwise correct plaintext, but with wrong second byte + # (0x00 instead of 0x02), and a 0x02 on third position, generates a + # random 11 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +1786550ce8d8433052e01ecba8b76d3019f1355b212ac9d0f5191b023325a7e7714b7802f8e9 +a17c4cb3cd3a84041891471b10ca1fcfb5d041d34c82e6d0011cf4dc76b90e9c2e0743590579 +d55bcd7857057152c4a8040361343d1d22ba677d62b011407c652e234b1d663af25e2386251d +7409190f19fc8ec3f9374fdf1254633874ce2ec2bff40ad0cb473f9761ec7b68da45a4bd5e33 +f5d7dac9b9a20821df9406b653f78a95a6c0ea0a4d57f867e4db22c17bf9a12c150f809a7b72 +b6db86c22a8732241ebf3c6a4f2cf82671d917aba8bc61052b40ccddd743a94ea9b538175106 +201971cca9d136d25081739aaf6cd18b2aecf9ad320ea3f89502f955""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:3], b'\x00\x00\x02') + self.assertEqual(dec[-12:], b'\x00lorem ipsum') + + plaintext = a2b_hex(remove_whitespace("3d4a054d9358209e9cbbb9")) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'lorem ipsum') + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long_null_byte_first_byte_of_padding(self): + # an otherwise correct plaintext, but with a null byte on third + # position (first byte of padding), generates a random 11 byte + # long payload + ciphertext = a2b_hex(remove_whitespace(""" +179598823812d2c58a7eb50521150a48bcca8b4eb53414018b6bca19f4801456c5e36a940037 +ac516b0d6412ba44ec6b4f268a55ef1c5ffbf18a2f4e3522bb7b6ed89774b79bffa22f7d3102 +165565642de0d43a955e96a1f2e80e5430671d7266eb4f905dc8ff5e106dc5588e5b0289e49a +4913940e392a97062616d2bda38155471b7d360cfb94681c702f60ed2d4de614ea72bf1c5316 +0e63179f6c5b897b59492bee219108309f0b7b8cb2b136c346a5e98b8b4b8415fb1d713bae06 +7911e3057f1c335b4b7e39101eafd5d28f0189037e4334f4fdb9038427b1d119a6702aa82333 +19cc97d496cc289ae8c956ddc84042659a2d43d6aa22f12b81ab884e""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:3], b'\x00\x02\x00') + self.assertEqual(dec[-12:], b'\x00lorem ipsum') + + plaintext = a2b_hex("1f037dd717b07d3e7f7359") + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b"lorem ipsum") + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long_null_byte_at_eight_byte_of_padding(self): + # an otherwise correct plaintext, but with a null byte on tenth + # position (eight byte of padding), generates a random 11 byte long + # plaintext + ciphertext = a2b_hex(remove_whitespace(""" +a7a340675a82c30e22219a55bc07cdf36d47d01834c1834f917f18b517419ce9de2a96460e74 +5024436470ed85e94297b283537d52189c406a3f533cb405cc6a9dba46b482ce98b6e3dd52d8 +fce2237425617e38c11fbc46b61897ef200d01e4f25f5f6c4c5b38cd0de38ba11908b86595a8 +036a08a42a3d05b79600a97ac18ba368a08d6cf6ccb624f6e8002afc75599fba4de3d4f3ba7d +208391ebe8d21f8282b18e2c10869eb2702e68f9176b42b0ddc9d763f0c86ba0ff92c957aaea +b76d9ab8da52ea297ec11d92d770146faa1b300e0f91ef969b53e7d2907ffc984e9a9c9d11fb +7d6cba91972059b46506b035efec6575c46d7114a6b935864858445f""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x00\x02') + self.assertNotEqual(dec[2:3], b'\x00') + self.assertEqual(dec[9:10], b'\x00') + self.assertEqual(dec[-12:], b'\x00lorem ipsum') + + plaintext = a2b_hex("63cb0bf65fc8255dd29e17") + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b"lorem ipsum") + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long_missing_null_separator(self): + # an otherwise correct plaintext, but with missing zero separator + # decrypts to 11 byte random synthethic plaintext + ciphertext = a2b_hex(remove_whitespace(""" +3d1b97e7aa34eaf1f4fc171ceb11dcfffd9a46a5b6961205b10b302818c1fcc9f4ec78bf18ea +0cee7e9fa5b16fb4c611463b368b3312ac11cf9c06b7cf72b54e284848a508d3f02328c62c29 +99d0fb60929f81783c7a256891bc2ff4d91df2af96a24fc5701a1823af939ce6dbdc510608e3 +d41eec172ad2d51b9fc61b4217c923cadcf5bac321355ef8be5e5f090cdc2bd0c697d9058247 +db3ad613fdce87d2955a6d1c948a5160f93da21f731d74137f5d1f53a1923adb513d2e6e1589 +d44cc079f4c6ddd471d38ac82d20d8b1d21f8d65f3b6907086809f4123e08d86fb38729585de +026a485d8f0e703fd4772f6668febf67df947b82195fa3867e3a3065""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x00\x02') + for val in dec[2:-12]: + self.assertNotEqual(val, 0) + self.assertEqual(dec[-12:], b'\x01lorem ipsum') + + plaintext = a2b_hex(remove_whitespace("6f09a0b62699337c497b0b")) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'lorem ipsum') + self.assertEqual(msg, plaintext) + + +class TestRSA2049Decrypt(unittest.TestCase): + @classmethod + def setUpClass(cls): + priv_key = """ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEBVfiJVWoXdfHHp3hqULGLwoyemG7eVmfKs5uEEk6Q66dcHbCD +rD5EO7qU3CNWD3XjqBaToqQ73HQm2MTq/mjIXeD+dX9uSbue1EfmAkMIANuwTOsi +5/pXoY0zj7ZgJs20Z+cMwEDn02fvQDx78ePfYkZQCUYx8h6v0vtbyRX/BDeazRES +9zLAtGYHwXjTiiD1LtpQny+cBAXVEGnoDM+UFVTQRwRnUFw89UHqCJffyfQAzssp +j/x1M3LZ9pM68XTMQO2W1GcDFzO5f4zd0/krw6A+qFdsQX8kAHteT3UBEFtUTen6 +3N/635jftLsFuBmfP4Ws/ZH3qaCUuaOD9QSQlwIDAQABAoIBAQEZwrP1CnrWFSZ5 +1/9RCVisLYym8AKFkvMy1VoWc2F4qOZ/F+cFzjAOPodUclEAYBP5dNCj20nvNEyl +omo0wEUHBNDkIuDOI6aUJcFf77bybhBu7/ZMyLnXRC5NpOjIUAjq6zZYWaIpT6OT +e8Jr5WMy59geLBYO9jXMUoqnvlXmM6cj28Hha6KeUrKa7y+eVlT9wGZrsPwlSsvo +DmOHTw9fAgeC48nc/CUg0MnEp7Y05FA/u0k+Gq/us/iL16EzmHJdrm/jmed1zV1M +8J/IODR8TJjasaSIPM5iBRNhWvqhCmM2jm17ed9BZqsWJznvUVpEAu4eBgHFpVvH +HfDjDt+BAoGBAYj2k2DwHhjZot4pUlPSUsMeRHbOpf97+EE99/3jVlI83JdoBfhP +wN3sdw3wbO0GXIETSHVLNGrxaXVod/07PVaGgsh4fQsxTvasZ9ZegTM5i2Kgg8D4 +dlxa1A1agfm73OJSftfpUAjLECnLTKvR+em+38KGyWVSJV2n6rGSF473AoGBAN7H +zxHa3oOkxD0vgBl/If1dRv1XtDH0T+gaHeN/agkf/ARk7ZcdyFCINa3mzF9Wbzll +YTqLNnmMkubiP1LvkH6VZ+NBvrxTNxiWJfu+qx87ez+S/7JoHm71p4SowtePfC2J +qqok0s7b0GaBz+ZcNse/o8W6E1FiIi71wukUyYNhAoGAEgk/OnPK7dkPYKME5FQC ++HGrMsjJVbCa9GOjvkNw8tVYSpq7q2n9sDHqRPmEBl0EYehAqyGIhmAONxVUbIsL +ha0m04y0MI9S0H+ZRH2R8IfzndNAONsuk46XrQU6cfvtZ3Xh3IcY5U5sr35lRn2c +ut3H52XIWJ4smN/cJcpOyoECgYEAjM5hNHnPlgj392wkXPkbtJXWHp3mSISQVLTd +G0MW8/mBQg3AlXi/eRb+RpHPrppk5jQLhgMjRSPyXXe2amb8PuWTqfGN6l32PtX3 +3+udILpppb71Wf+w7JTbcl9v9uq7o9SVR8DKdPA+AeweSQ0TmqCnlHuNZizOSjwP +G16GF0ECgYEA+ZWbNMS8qM5IiHgbMbHptdit9dDT4+1UXoNn0/hUW6ZEMriHMDXv +iBwrzeANGAn5LEDYeDe1xPms9Is2uNxTpZVhpFZSNALR6Po68wDlTJG2PmzuBv5t +5mbzkpWCoD4fRU53ifsHgaTW+7Um74gWIf0erNIUZuTN2YrtEPTnb3k= +-----END RSA PRIVATE KEY----- +""" + cls.priv_key = parsePEMKey(priv_key, private=True) + + pub_key = """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEBVfiJVWoXdfHHp3hqULGL +woyemG7eVmfKs5uEEk6Q66dcHbCDrD5EO7qU3CNWD3XjqBaToqQ73HQm2MTq/mjI +XeD+dX9uSbue1EfmAkMIANuwTOsi5/pXoY0zj7ZgJs20Z+cMwEDn02fvQDx78ePf +YkZQCUYx8h6v0vtbyRX/BDeazRES9zLAtGYHwXjTiiD1LtpQny+cBAXVEGnoDM+U +FVTQRwRnUFw89UHqCJffyfQAzsspj/x1M3LZ9pM68XTMQO2W1GcDFzO5f4zd0/kr +w6A+qFdsQX8kAHteT3UBEFtUTen63N/635jftLsFuBmfP4Ws/ZH3qaCUuaOD9QSQ +lwIDAQAB +-----END PUBLIC KEY----- +""" + cls.pub_key = parsePEMKey(pub_key, public=True) + + def test_sanity(self): + self.assertIsNotNone(self.priv_key) + self.assertIsNotNone(self.pub_key) + + self.assertEqual( + self.priv_key.d, + bytesToNumber(a2b_hex( + "0119c2b3f50a7ad6152679d7ff510958ac2d8ca6f0028592f332d55a1673" + "6178a8e67f17e705ce300e3e87547251006013f974d0a3db49ef344ca5a2" + "6a34c0450704d0e422e0ce23a69425c15fefb6f26e106eeff64cc8b9d744" + "2e4da4e8c85008eaeb365859a2294fa3937bc26be56332e7d81e2c160ef6" + "35cc528aa7be55e633a723dbc1e16ba29e52b29aef2f9e5654fdc0666bb0" + "fc254acbe80e63874f0f5f020782e3c9dcfc2520d0c9c4a7b634e4503fbb" + "493e1aafeeb3f88bd7a13398725dae6fe399e775cd5d4cf09fc838347c4c" + "98dab1a4883cce620513615afaa10a63368e6d7b79df4166ab162739ef51" + "5a4402ee1e0601c5a55bc71df0e30edf81"))) + + def test_simple(self): + msg = b'some long message' + self.assertEqual( + msg, + self.priv_key.decrypt(self.pub_key.encrypt(msg))) + + def test_with_ciphertext_length_from_third_prf_value(self): + # malformed plaintext that generates a fake plaintext of length + # specified by 3rd length from the end of PRF output + ciphertext = a2b_hex(remove_whitespace(""" +00b26f6404b82649629f2704494282443776929122e279a9cf30b0c6fe8122a0a9042870d97c +c8ef65490fe58f031eb2442352191f5fbc311026b5147d32df914599f38b825ebb824af0d63f +2d541a245c5775d1c4b78630e4996cc5fe413d38455a776cf4edcc0aa7fccb31c584d60502ed +2b77398f536e137ff7ba6430e9258e21c2db5b82f5380f566876110ac4c759178900fbad7ab7 +0ea07b1daf7a1639cbb4196543a6cbe8271f35dddb8120304f6eef83059e1c5c5678710f904a +6d760c4d1d8ad076be17904b9e69910040b47914a0176fb7eea0c06444a6c4b86d674d19a556 +a1de5490373cb01ce31bbd15a5633362d3d2cd7d4af1b4c5121288b894""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertEqual(dec[-2:], b'\xc8\xfa') + + plaintext = b'\x42' + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_11_bytes_long(self): + # a valid ciphertext that decrypts to 11 byte long message + ciphertext = a2b_hex(remove_whitespace(""" +013300edbf0bb3571e59889f7ed76970bf6d57e1c89bbb6d1c3991d9df8e65ed54b556d928da +7d768facb395bbcc81e9f8573b45cf8195dbd85d83a59281cddf4163aec11b53b4140053e3bd +109f787a7c3cec31d535af1f50e0598d85d96d91ea01913d07097d25af99c67464ebf2bb396f +b28a9233e56f31f7e105d71a23e9ef3b736d1e80e713d1691713df97334779552fc94b40dd73 +3c7251bc522b673d3ec9354af3dd4ad44fa71c0662213a57ada1d75149697d0eb55c053aaed5 +ffd0b815832f454179519d3736fb4faf808416071db0d0f801aca8548311ee708c131f4be658 +b15f6b54256872c2903ac708bd43b017b073b5707bc84c2cd9da70e967""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + plaintext = b'lorem ipsum' + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_11_bytes_long_with_null_padded_ciphertext(self): + # a valid ciphertext that starts with a null byte, decrypts to 11 byte + # long value + ciphertext = a2b_hex(remove_whitespace(""" +0002aadf846a329fadc6760980303dbd87bfadfa78c2015ce4d6c5782fd9d3f1078bd3c0a2c5 +bfbdd1c024552e5054d98b5bcdc94e476dd280e64d650089326542ce7c61d4f1ab40004c2e6a +88a883613568556a10f3f9edeab67ae8dddc1e6b0831c2793d2715de943f7ce34c5c05d1b09f +14431fde566d17e76c9feee90d86a2c158616ec81dda0c642f58c0ba8fa4495843124a7235d4 +6fb4069715a51bf710fd024259131ba94da73597ace494856c94e7a3ec261545793b0990279b +15fa91c7fd13dbfb1df2f221dab9fa9f7c1d21e48aa49f6aaecbabf5ee76dc6c2af2317ffb4e +303115386a97f8729afc3d0c89419669235f1a3a69570e0836c79fc162""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + plaintext = b'lorem ipsum' + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_11_bytes_long_with_double_null_padded_ciphertext(self): + # a valid ciphertext that starts with two null bytes, decrypts to + # 11 byte long value + ciphertext = a2b_hex(remove_whitespace(""" +0000f36da3b72d8ff6ded74e7efd08c01908f3f5f0de7b55eab92b5f875190809c39d4162e1e +6649618f854fd84aeab03970d16bb814e999852c06de38d82b95c0f32e2a7b5714021fe30338 +9be9c0eac24c90a6b7210f929d390fabf903d44e04110bb7a7fd6c383c275804721efa6d7c93 +aa64c0bb2b18d97c5220a846c66a4895ae52adddbe2a9996825e013585adcec4b32ba61d7827 +37bd343e5fabd68e8a95b8b1340318559860792dd70dffbe05a1052b54cbfb48cfa7bb3c19ce +a52076bddac5c25ee276f153a610f6d06ed696d192d8ae4507ffae4e5bdda10a625d6b67f32f +7cffcd48dee2431fe66f6105f9d17e611cdcc674868e81692a360f4052""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + plaintext = b'lorem ipsum' + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long(self): + # a random ciphertext that generates a fake 11 byte plaintext + # and fails the padding check + ciphertext = a2b_hex(remove_whitespace(""" +00f910200830fc8fff478e99e145f1474b312e2512d0f90b8cef77f8001d09861688c156d1cb +af8a8957f7ebf35f724466952d0524cad48aad4fba1e45ce8ea27e8f3ba44131b7831b62d60c +0762661f4c1d1a88cd06263a259abf1ba9e6b0b172069afb86a7e88387726f8ab3adb30bfd6b +3f6be6d85d5dfd044e7ef052395474a9cbb1c3667a92780b43a22693015af6c513041bdaf87d +43b24ddd244e791eeaea1066e1f4917117b3a468e22e0f7358852bb981248de4d720add2d15d +ccba6280355935b67c96f9dcb6c419cc38ab9f6fba2d649ef2066e0c34c9f788ae49babd9025 +fa85b21113e56ce4f43aa134c512b030dd7ac7ce82e76f0be9ce09ebca""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertNotEqual(dec[-12:-11], b'\x00') + + plaintext = a2b_hex(remove_whitespace("1189b6f5498fd6df532b00")) + + self.assertEqual(len(plaintext), 11) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'lorem ipsum') + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long_wrong_version_byte(self): + # an otherwise correct plaintext, but with wrong first byte + # (0x01 instead of 0x00), generates a random 11 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +002c9ddc36ba4cf0038692b2d3a1c61a4bb3786a97ce2e46a3ba74d03158aeef456ce0f4db04 +dda3fe062268a1711250a18c69778a6280d88e133a16254e1f0e30ce8dac9b57d2e39a2f7d7b +e3ee4e08aec2fdbe8dadad7fdbf442a29a8fb40857407bf6be35596b8eefb5c2b3f58b894452 +c2dc54a6123a1a38d642e23751746597e08d71ac92704adc17803b19e131b4d1927881f43b02 +00e6f95658f559f912c889b4cd51862784364896cd6e8618f485a992f82997ad6a0917e32ae5 +872eaf850092b2d6c782ad35f487b79682333c1750c685d7d32ab3e1538f31dcaa5e7d5d2825 +875242c83947308dcf63ba4bfff20334c9c140c837dbdbae7a8dee72ff""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x01\x02') + self.assertEqual(dec[-12:], b'\x00lorem ipsum') + + plaintext = a2b_hex(remove_whitespace("f6d0f5b78082fe61c04674")) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'lorem ipsum') + self.assertEqual(msg, plaintext) + + def test_negative_11_byte_long_wrong_type_byte(self): + # an otherwise correct plaintext, but with wrong second byte + # (0x01 instead of 0x02), generates a random 11 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +00c5d77826c1ab7a34d6390f9d342d5dbe848942e2618287952ba0350d7de6726112e9cebc39 +1a0fae1839e2bf168229e3e0d71d4161801509f1f28f6e1487ca52df05c466b6b0a6fbbe57a3 +268a970610ec0beac39ec0fa67babce1ef2a86bf77466dc127d7d0d2962c20e66593126f2768 +63cd38dc6351428f884c1384f67cad0a0ffdbc2af16711fb68dc559b96b37b4f04cd133ffc7d +79c43c42ca4948fa895b9daeb853150c8a5169849b730cc77d68b0217d6c0e3dbf38d751a199 +8186633418367e7576530566c23d6d4e0da9b038d0bb5169ce40133ea076472d055001f01356 +45940fd08ea44269af2604c8b1ba225053d6db9ab43577689401bdc0f3""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x00\x01') + self.assertEqual(dec[-12:], b'\x00lorem ipsum') + + plaintext = a2b_hex(remove_whitespace("1ab287fcef3ff17067914d")) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'lorem ipsum') + self.assertEqual(msg, plaintext) + + +class TestRSA3072Decrypt(unittest.TestCase): + @classmethod + def setUpClass(cls): + priv_key = """ +-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEAr9ccqtXp9bjGw2cHCkfxnX5mrt4YpbJ0H7PE0zQ0VgaSotkJ +72iI7GAv9rk68ljudDA8MBr81O2+xDMR3cjdvwDdu+OG0zuNDiKxtEk23EiYcbhS +N7NM50etj9sMTk0dqnqt8HOFxchzLMt9Wkni5QyIPH16wQ7Wp02ayQ35EpkFoX1K +CHIQ/Hi20EseuWlILBGm7recUOWxbz8lT3VxUosvFxargW1uygcnveqYBZMpcw64 +wzznHWHdSsOTtiVuB6wdEk8CANHD4FpMG8fx7S/IPlcZnP5ZCLEAh+J/vZfSwkIU +YZxxR8j778o5vCVnYqaCNTH34jTWjq56DZ+vEN0V6VI3gMfVrlgJStUlqQY7TDP5 +XhAG2i6xLTdDaJSVwfICPkBzU8XrPkyhxIz/gaEJANFIIOuAGvTxpZbEuc6aUx/P +ilTZ/9ckJYtu7CAQjfb9/XbUrgO6fqWY3LDkooCElYcob01/JWzoXl61Z5sdrMH5 +CVZJty5foHKusAN5AgMBAAECggGAJRfqyzr+9L/65gOY35lXpdKhVKgzaNjhWEKy +9Z7gn3kZe9LvHprdr4eG9rQSdEdAXjBCsh8vULeqc3cWgMO7y2wiWl1f9rVsRxwY +gqCjOwrxZaPtbCSdx3g+a8dYrDfmVy0z/jJQeO2VJlDy65YEkC75mlEaERnRPE/J +pDoXXc37+xoUAP4XCTtpzTzbiV9lQy6iGV+QURxzNrWKaF2s/y2vTF6S5WWxZlrm +DlErqplluAjV/xGc63zWksv5IAZ6+s2An2a+cG2iaBCseQ2xVslI5v5YG8mEkVf0 +2kk/OmSwxuEZ4DGxB/hDbOKRYLRYuPnxCV/esZJjOE/1OHVXvE8QtANN6EFwO60s +HnacI4U+tjCjbRBh3UbipruvdDqX8LMsNvUMGjci3vOjlNkcLgeL8J15Xs3l5WuC +Avl0Am91/FbpoN1qiPLny3jvEpjMbGUgfKRb03GIgHtPzbHmDdjluFZI+376i2/d +RI85dBqNmAn+Fjrz3kW6wkpahByBAoHBAOSj2DDXPosxxoLidP/J/RKsMT0t0FE9 +UFcNt+tHYv6hk+e7VAuUqUpd3XQqz3P13rnK4xvSOsVguyeU/WgmH4ID9XGSgpBP +Rh6s7izn4KAJeqfI26vTPxvyaZEqB4JxT6k7SerENus95zSn1v/f2MLBQ16EP8cJ ++QSOVCoZfEhUK+srherQ9eZKpj0OwBUrP4VhLdymv96r8xddWX1AVj4OBi2RywKI +gAgv6fjwkb292jFu6x6FjKRNKwKK6c3jqQKBwQDE4c0Oz0KYYV4feJun3iL9UJSv +StGsKVDuljA4WiBAmigMZTii/u0DFEjibiLWcJOnH53HTr0avA6c6D1nCwJ2qxyF +rHNN2L+cdMx/7L1zLR11+InvRgpIGbpeGwHeIzJVUYG3b6llRJMZimBvAMr9ipM1 +bkVvIjt1G9W1ypeuKzm6d/t8F0yC7AIYZWDV4nvxiiY8whLZzGawHR2iZz8pfUwb +7URbTvxdsGE27Kq9gstU0PzEJpnU1goCJ7/gA1ECgcBA8w5B6ZM5xV0H5z6nPwDm +IgYmw/HucgV1hU8exfuoK8wxQvTACW4B0yJKkrK11T1899aGG7VYRn9D4j4OLO48 +Z9V8esseJXbc1fEezovvymGOci984xiFXtqAQzk44+lmQJJh33VeZApe2eLocvVH +ddEmc1kOuJWFpszf3LeCcG69cnKrXsrLrZ8Frz//g3aa9B0sFi5hGeWHWJxISVN2 +c1Nr9IN/57i/GqVTcztjdCAcdM7Tr8phDg7OvRlnxGkCgcEAuYhMFBuulyiSaTff +/3ZvJKYOJ45rPkEFGoD/2ercn+RlvyCYGcoAEjnIYVEGlWwrSH+b0NlbjVkQsD6O +to8CeE/RpgqX8hFCqC7NE/RFp8cpDyXy3j/zqnRMUyhCP1KNuScBBZs9V8gikxv6 +ukBWCk3PYbeTySHKRBbB8vmCrMfhM96jaBIQsQO1CcZnVceDo1/bnsAIwaREVMxr +Q8LmG7QOx/Z0x1MMsUFoqzilwccC09/JgxMZPh+h+Nv6jiCxAoHBAOEqQgFAfSdR +ya60LLH55q803NRFMamuKiPbVJLzwiKfbjOiiopmQOS/LxxqIzeMXlYV4OsSvxTo +G7mcTOFRtU5hKCK+t8qeQQpa/dsMpiHllwArnRyBjIVgL5lFKRpHUGLsavU/T1IH +mtgaxZo32dXvcAh1+ndCHVBwbHTOF4conA+g+Usp4bZSSWn5nU4oIizvSVpG7SGe +0GngdxH9Usdqbvzcip1EKeHRTZrHIEYmB+x0LaRIB3dwZNidK3TkKw== +-----END RSA PRIVATE KEY-----""" + cls.priv_key = parsePEMKey(priv_key, private=True) + + pub_key = """ +-----BEGIN PUBLIC KEY----- +MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAr9ccqtXp9bjGw2cHCkfx +nX5mrt4YpbJ0H7PE0zQ0VgaSotkJ72iI7GAv9rk68ljudDA8MBr81O2+xDMR3cjd +vwDdu+OG0zuNDiKxtEk23EiYcbhSN7NM50etj9sMTk0dqnqt8HOFxchzLMt9Wkni +5QyIPH16wQ7Wp02ayQ35EpkFoX1KCHIQ/Hi20EseuWlILBGm7recUOWxbz8lT3Vx +UosvFxargW1uygcnveqYBZMpcw64wzznHWHdSsOTtiVuB6wdEk8CANHD4FpMG8fx +7S/IPlcZnP5ZCLEAh+J/vZfSwkIUYZxxR8j778o5vCVnYqaCNTH34jTWjq56DZ+v +EN0V6VI3gMfVrlgJStUlqQY7TDP5XhAG2i6xLTdDaJSVwfICPkBzU8XrPkyhxIz/ +gaEJANFIIOuAGvTxpZbEuc6aUx/PilTZ/9ckJYtu7CAQjfb9/XbUrgO6fqWY3LDk +ooCElYcob01/JWzoXl61Z5sdrMH5CVZJty5foHKusAN5AgMBAAE= +-----END PUBLIC KEY-----""" + cls.pub_key = parsePEMKey(pub_key, public=True) + + def test_sanity(self): + self.assertIsNotNone(self.priv_key) + self.assertIsNotNone(self.pub_key) + + self.assertEqual( + self.priv_key.d, + bytesToNumber(a2b_hex( + "2517eacb3afef4bffae60398df9957a5d2a154a83368d8e15842b2f59ee0" + "9f79197bd2ef1e9addaf8786f6b4127447405e3042b21f2f50b7aa737716" + "80c3bbcb6c225a5d5ff6b56c471c1882a0a33b0af165a3ed6c249dc7783e" + "6bc758ac37e6572d33fe325078ed952650f2eb9604902ef99a511a1119d1" + "3c4fc9a43a175dcdfbfb1a1400fe17093b69cd3cdb895f65432ea2195f90" + "511c7336b58a685dacff2daf4c5e92e565b1665ae60e512baa9965b808d5" + "ff119ceb7cd692cbf920067afacd809f66be706da26810ac790db156c948" + "e6fe581bc9849157f4da493f3a64b0c6e119e031b107f8436ce29160b458" + "b8f9f1095fdeb19263384ff5387557bc4f10b4034de841703bad2c1e769c" + "23853eb630a36d1061dd46e2a6bbaf743a97f0b32c36f50c1a3722def3a3" + "94d91c2e078bf09d795ecde5e56b8202f974026f75fc56e9a0dd6a88f2e7" + "cb78ef1298cc6c65207ca45bd37188807b4fcdb1e60dd8e5b85648fb7efa" + "8b6fdd448f39741a8d9809fe163af3de45bac24a5a841c81"))) + + def test_simple(self): + msg = b'some long message' + self.assertEqual( + msg, + self.priv_key.decrypt(self.pub_key.encrypt(msg))) + + def test_simple_max_len(self): + msg = b's' * (numBytes(self.pub_key.n)-2-8-1) + self.assertEqual( + msg, + self.priv_key.decrypt(self.pub_key.encrypt(msg))) + + def test_simple_with_empty(self): + self.assertEqual( + b'', + self.priv_key.decrypt(self.pub_key.encrypt(b''))) + + def test_negative_with_zero_length(self): + # and invalid ciphertext that generates a synthethic plaintext + # that's zero bytes in length + ciphertext = a2b_hex(remove_whitespace(""" +5e956cd9652f4a2ece902931013e09662b6a9257ad1e987fb75f73a0606df2a4b04789770820 +c2e02322c4e826f767bd895734a01e20609c3be4517a7a2a589ea1cdc137beb73eb38dac781b +52e863de9620f79f9b90fd5b953651fcbfef4a9f1cc07421d511a87dd6942caab6a5a0f4df47 +3e62defb529a7de1509ab99c596e1dff1320402298d8be73a896cc86c38ae3f2f576e9ea70cc +28ad575cb0f854f0be43186baa9c18e29c47c6ca77135db79c811231b7c1730955887d321fdc +06568382b86643cf089b10e35ab23e827d2e5aa7b4e99ff2e914f302351819eb4d1693243b35 +f8bf1d42d08f8ec4acafa35f747a4a975a28643ec630d8e4fa5be59d81995660a14bb64c1fea +5146d6b11f92da6a3956dd5cb5e0d747cf2ea23f81617769185336263d46ef4c144b754de62a +6337342d6c85a95f19f015724546ee3fc4823eca603dbc1dc01c2d5ed50bd72d8e96df2dc048 +edde0081284068283fc5e73a6139851abf2f29977d0b3d160c883a42a37efba1be05c1a0b174 +1d7ddf59""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(b'', msg) + + def test_negative_with_max_len_plus_one_in_first_value_from_prf(self): + # an invalid ciphertext that generates last length that's one byte + # too long for the key size, so the second to last value needs to get + # used + ciphertext = a2b_hex(remove_whitespace(""" +7db0390d75fcf9d4c59cf27b264190d856da9abd11e92334d0e5f71005cfed865a711dfa28b7 +91188374b61916dbc11339bf14b06f5f3f68c206c5607380e13da3129bfb744157e1527dd6fd +f6651248b028a496ae1b97702d44706043cdaa7a59c0f41367303f21f268968bf3bd2904db3a +e5239b55f8b438d93d7db9d1666c071c0857e2ec37757463769c54e51f052b2a71b04c2869e9 +e7049a1037b8429206c99726f07289bac18363e7eb2a5b417f47c37a55090cda676517b3549c +873f2fe95da9681752ec9864b069089a2ed2f340c8b04ee00079055a817a3355b46ac7dc00d1 +7f4504ccfbcfcadb0c04cb6b22069e179385ae1eafabad5521bac2b8a8ee1dfff59a22eb3fda +cfc87175d10d7894cfd869d056057dd9944b869c1784fcc27f731bc46171d39570fbffbadf08 +2d33f6352ecf44aca8d9478e53f5a5b7c852b401e8f5f74da49da91e65bdc97765a9523b7a08 +85a6f8afe5759d58009fbfa837472a968e6ae92026a5e0202a395483095302d6c3985b5f5831 +c521a271""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + + plaintext = a2b_hex("56a3bea054e01338be9b7d7957539c") + + self.assertEqual(len(plaintext), 15) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_negative_with_max_len(self): + # an invalid ciphertext that generates a plaintext of maximum size + # for this key size + ciphertext = a2b_hex(remove_whitespace(""" +1715065322522dff85049800f6a29ab5f98c465020467414b2a44127fe9446da47fa18047900 +f99afe67c2df6f50160bb8e90bff296610fde632b3859d4d0d2e644f23835028c46cca01b84b +88231d7e03154edec6627bcba23de76740d839851fa12d74c8f92e540c73fe837b91b7d699b3 +11997d5f0f7864c486d499c3a79c111faaacbe4799597a25066c6200215c3d158f3817c1aa57 +f18bdaad0be1658da9da93f5cc6c3c4dd72788af57adbb6a0c26f42d32d95b8a4f95e8c6feb2 +f8a5d53b19a50a0b7cbc25e055ad03e5ace8f3f7db13e57759f67b65d143f08cca15992c6b2a +ae643390483de111c2988d4e76b42596266005103c8de6044fb7398eb3c28a864fa672de5fd8 +774510ff45e05969a11a4c7d3f343e331190d2dcf24fb9154ba904dc94af98afc5774a9617d0 +418fe6d13f8245c7d7626c176138dd698a23547c25f27c2b98ea4d8a45c7842b81888e4cc14e +5b72e9cf91f56956c93dbf2e5f44a8282a7813157fc481ff1371a0f66b31797e81ebdb09a673 +d4db96d6""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + + plaintext = a2b_hex(remove_whitespace(""" +7b036fcd6243900e4236c894e2462c17738acc87e01a76f4d95cb9a328d9acde81650283b8e8 +f60a217e3bdee835c7b222ad4c85d0acdb9a309bd2a754609a65dec50f3aa04c6d5891034566 +b9563d42668ede1f8992b17753a2132e28970584e255efc8b45a41c5dbd7567f014acec5fe6f +db6d484790360a913ebb9defcd74ff377f2a8ba46d2ed85f733c9a3da08eb57ecedfafda8067 +78f03c66b2c5d2874cec1c291b2d49eb194c7b5d0dd2908ae90f4843268a2c45563092ade08a +cb6ab481a08176102fc803fbb2f8ad11b0e1531bd37df543498daf180b12017f4d4d426ca29b +4161075534bfb914968088a9d13785d0adc0e2580d3548494b2a9e91605f2b27e6cc701c796f +0de7c6f471f6ab6cb9272a1ed637ca32a60d117505d82af3c1336104afb537d01a8f70b510e1 +eebf4869cb976c419473795a66c7f5e6e20a8094b1bb603a74330c537c5c0698c31538bd2e13 +8c1275a1bdf24c5fa8ab3b7b526324e7918a382d1363b3d463764222150e04""")) + self.assertEqual(len(plaintext), 373) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_9_bytes_long(self): + ciphertext = a2b_hex(remove_whitespace(""" +6c60845a854b4571f678941ae35a2ac03f67c21e21146f9db1f2306be9f136453b86ad55647d +4f7b5c9e62197aaff0c0e40a3b54c4cde14e774b1c5959b6c2a2302896ffae1f73b00b862a20 +ff4304fe06cea7ff30ecb3773ca9af27a0b54547350d7c07dfb0a39629c7e71e83fc5af9b2ad +baf898e037f1de696a3f328cf45af7ec9aff7173854087fb8fbf34be981efbd8493f9438d1b2 +ba2a86af082662aa46ae9adfbec51e5f3d9550a4dd1dcb7c8969c9587a6edc82a8cabbc785c4 +0d9fbd12064559fb769450ac3e47e87bc046148130d7eaa843e4b3ccef3675d0630500803cb7 +ffee3882378c1a404e850c3e20707bb745e42b13c18786c4976076ed9fa8fd0ff15e571bef02 +cbbe2f90c908ac3734a433b73e778d4d17fcc28f49185ebc6e8536a06d293202d94496453bfd +f1c2c7833a3f99fa38ca8a81f42eaa529d603b890308a319c0ab63a35ff8ebac965f6278f5a7 +e5d622be5d5fe55f0ca3ec993d55430d2bf59c5d3e860e90c16d91a04596f6fdf60d89ed95d8 +8c036dde""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + plaintext = b'forty two' + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_9_bytes_long_with_null_padded_ciphertext(self): + # a valid ciphertext that starts with a null byte and decrypts to + # 9 byte long value + ciphertext = a2b_hex(remove_whitespace(""" +00f4d565a3286784dbb85327db8807ae557ead229f92aba945cecda5225f606a7d6130edeeb6 +f26724d1eff1110f9eb18dc3248140ee3837e6688391e78796c526791384f045e21b6b853fb6 +342a11f309eb77962f37ce23925af600847fbd30e6e07e57de50b606e6b7f288cc777c1a6834 +f27e6edace508452128916eef7788c8bb227e3548c6a761cc4e9dd1a3584176dc053ba3500ad +b1d5e1611291654f12dfc5722832f635db3002d73f9defc310ace62c63868d341619c7ee15b2 +0243b3371e05078e11219770c701d9f341af35df1bc729de294825ff2e416aa1152661285277 +7eb131f9c45151eb144980d70608d2fc4043477368369aa0fe487a48bd57e66b00c3c58f9415 +49f5ec050fca64449debe7a0c4ac51e55cb71620a70312aa4bd85fac1410c9c7f9d6ec610b7d +11bf8faeffa20255d1a1bead9297d0aa8765cd2805847d639bc439f4a6c896e2008f746f9590 +ff4596de5ddde000ed666c452c978043ff4298461eb5a26d5e63d821438627f91201924bf7f2 +aeee1727""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + plaintext = b'forty two' + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_positive_9_bytes_long_with_double_null_padded_ciphertext(self): + # a valid ciphertext that starts with two null bytes and decrypts to + # 9 byte long value + ciphertext = a2b_hex(remove_whitespace(""" +00001ec97ac981dfd9dcc7a7389fdfa9d361141dac80c23a060410d472c16094e6cdffc0c368 +4d84aa402d7051dfccb2f6da33f66985d2a259f5b7fbf39ac537e95c5b7050eb18844a0513ab +ef812cc8e74a3c5240009e6e805dcadf532bc1a2702d5acc9e585fad5b89d461fcc1397351cd +ce35171523758b171dc041f412e42966de7f94856477356d06f2a6b40e3ff0547562a4d91bbf +1338e9e049facbee8b20171164505468cd308997447d3dc4b0acb49e7d368fedd8c734251f30 +a83491d2506f3f87318cc118823244a393dc7c5c739a2733d93e1b13db6840a9429947357f47 +b23fbe39b7d2d61e5ee26f9946c4632f6c4699e452f412a26641d4751135400713cd56ec66f0 +370423d55d2af70f5e7ad0adea8e4a0d904a01e4ac272eba4af1a029dd53eb71f115bf31f7a6 +c8b19a6523adeecc0d4c3c107575e38572a8f8474ccad163e46e2e8b08111132aa97a16fb588 +c9b7e37b3b3d7490381f3c55d1a9869a0fd42cd86fed59ecec78cb6b2dfd06a497f5afe34196 +91314ba0""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + plaintext = b'forty two' + + msg = self.priv_key.decrypt(ciphertext) + + self.assertEqual(msg, plaintext) + + def test_negative_9_bytes_long(self): + ciphertext = a2b_hex(remove_whitespace(""" +5c8555f5cef627c15d37f85c7f5fd6e499264ea4b8e3f9112023aeb722eb38d8eac2be3751fd +5a3785ab7f2d59fa3728e5be8c3de78a67464e30b21ee23b5484bb3cd06d0e1c6ad25649c851 +8165653eb80488bfb491b20c04897a6772f69292222fc5ef50b5cf9efc6d60426a449b6c4895 +69d48c83488df629d695653d409ce49a795447fcec2c58a1a672e4a391401d428baaf781516e +11e323d302fcf20f6eab2b2dbe53a48c987e407c4d7e1cb41131329138313d330204173a4f3f +f06c6fadf970f0ed1005d0b27e35c3d11693e0429e272d583e57b2c58d24315c397856b34485 +dcb077665592b747f889d34febf2be8fce66c265fd9fc3575a6286a5ce88b4b413a08efc57a0 +7a8f57a999605a837b0542695c0d189e678b53662ecf7c3d37d9dbeea585eebfaf79141118e0 +6762c2381fe27ca6288edddc19fd67cd64f16b46e06d8a59ac530f22cd83cc0bc4e37feb5201 +5cbb2283043ccf5e78a4eb7146827d7a466b66c8a4a4826c1bad68123a7f2d00fc1736525ff9 +0c058f56""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertNotEqual(dec[-10:-9], b'\x00') + + plaintext = a2b_hex(remove_whitespace("257906ca6de8307728")) + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'forty two') + self.assertEqual(msg, plaintext) + + def test_negative_9_bytes_long_from_second_prf_value(self): + # malformed plaintext that generates a fake plaintext of length + # specified by 2nd to last value from PRF + ciphertext = a2b_hex(remove_whitespace(""" +758c215aa6acd61248062b88284bf43c13cb3b3d02410be4238607442f1c0216706e21a03a2c +10eb624a63322d854da195c017b76fea83e274fa371834dcd2f3b7accf433fc212ad76c0bac3 +66e1ed32e25b279f94129be7c64d6e162adc08ccebc0cfe8e926f01c33ab9c065f0e0ac83ae5 +137a4cb66702615ad68a35707d8676d2740d7c1a954680c83980e19778ed11eed3a7c2dbdfc4 +61a9bbef671c1bc00c882d361d29d5f80c42bdf5efec886c34138f83369c6933b2ac4e93e764 +265351b4a0083f040e14f511f09b22f96566138864e4e6ff24da4810095da98e058541095153 +8ced2f757a277ff8e17172f06572c9024eeae503f176fd46eb6c5cd9ba07af11cde31dccac12 +eb3a4249a7bfd3b19797ad1656984bfcbf6f74e8f99d8f1ac420811f3d166d87f935ef15ae85 +8cf9e72c8e2b547bf16c3fb09a8c9bf88fd2e5d38bf24ed610896131a84df76b9f920fe76d71 +fff938e9199f3b8cd0c11fd0201f9139d7673a871a9e7d4adc3bbe360c8813617cd60a90128f +be34c9d5""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertNotEqual(dec[-10:-9], b'\x00') + + plaintext = a2b_hex(remove_whitespace("043383c929060374ed")) + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'forty two') + self.assertEqual(msg, plaintext) + + def test_negative_9_bytes_long_from_third_prf_value(self): + # malformed plaintext that generates a fake plaintext of length + # specified by 3rd to last value from PRF + ciphertext = a2b_hex(remove_whitespace(""" +7b22d5e62d287968c6622171a1f75db4b0fd15cdf3134a1895d235d56f8d8fe619f2bf486817 +4a91d7601a82975d2255190d28b869141d7c395f0b8c4e2be2b2c1b4ffc12ce749a6f6803d4c +fe7fba0a8d6949c04151f981c0d84592aa2ff25d1bd3ce5d10cb03daca6b496c6ad40d30bfa8 +acdfd02cdb9326c4bdd93b949c9dc46caa8f0e5f429785bce64136a429a3695ee674b647452b +ea1b0c6de9c5f1e8760d5ef6d5a9cfff40457b023d3c233c1dcb323e7808103e73963b2eafc9 +28c9eeb0ee3294955415c1ddd9a1bb7e138fecd79a3cb89c57bd2305524624814aaf0fd1acbf +379f7f5b39421f12f115ba488d380586095bb53f174fae424fa4c8e3b299709cd344b9f949b1 +ab57f1c645d7ed3c8f81d5594197355029fee8960970ff59710dc0e5eb50ea6f4c3938e3f89e +d7933023a2c2ddffaba07be147f686828bd7d520f300507ed6e71bdaee05570b27bc92741108 +ac2eb433f028e138dd6d63067bc206ea2d826a7f41c0d613daed020f0f30f4e272e9618e0a8c +39018a83""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertNotEqual(dec[0:1], b'\x00') + self.assertNotEqual(dec[1:2], b'\x02') + self.assertNotEqual(dec[-10:-9], b'\x00') + + plaintext = a2b_hex(remove_whitespace("70263fa6050534b9e0")) + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'forty two') + self.assertEqual(msg, plaintext) + + def test_negative_9_bytes_long_wrong_version_byte(self): + # an otherwise correct plaintext, but with wrong first byte + # (0x01 instead of 0x00), generates a random 9 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +6db80adb5ff0a768caf1378ecc382a694e7d1bde2eff4ba12c48aaf794ded7a994a5b2b57ace +c20dbec4ae385c9dd531945c0f197a5496908725fc99d88601a17d3bb0b2d38d2c1c3100f399 +55a4cb3dbed5a38bf900f23d91e173640e4ec655c84fdfe71fcdb12a386108fcf718c9b7af37 +d39703e882436224c877a2235e8344fba6c951eb7e2a4d1d1de81fb463ac1b880f6cc0e59ade +05c8ce35179ecd09546731fc07b141d3d6b342a97ae747e61a9130f72d37ac5a2c30215b6cbd +66c7db893810df58b4c457b4b54f34428247d584e0fa71062446210db08254fb9ead1ba1a393 +c724bd291f0cf1a7143f32df849051dc896d7d176fef3b57ab6dffd626d0c3044e9edb2e3d01 +2ace202d2581df01bec7e9aa0727a6650dd373d374f0bc0f4a611f8139dfe97d63e70c6188f4 +df5b672e47c51d8aa567097293fbff127c75ec690b43407578b73c85451710a0cece58fd497d +7f7bd36a8a92783ef7dc6265dff52aac8b70340b996508d39217f2783ce6fc91a1cc94bb2ac4 +87b84f62""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid in precisely + # one byte + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x01\x02') + for val in dec[2:-10]: + self.assertNotEqual(val, 0) + self.assertEqual(dec[-10:], b'\x00forty two') + + plaintext = a2b_hex("6d8d3a094ff3afff4c") + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'forty two') + self.assertEqual(msg, plaintext) + + def test_negative_9_bytes_long_wrong_type_byte(self): + # an otherwise correct plaintext, but with wrong second byte + # (0x01 instead of 0x02), generates a random 9 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +417328c034458563079a4024817d0150340c34e25ae16dcad690623f702e5c748a6ebb3419ff +48f486f83ba9df35c05efbd7f40613f0fc996c53706c30df6bba6dcd4a40825f96133f3c2163 +8a342bd4663dffbd0073980dac47f8c1dd8e97ce1412e4f91f2a8adb1ac2b1071066efe8d718 +bbb88ca4a59bd61500e826f2365255a409bece0f972df97c3a55e09289ef5fa815a2353ef393 +fd1aecfc888d611c16aec532e5148be15ef1bf2834b8f75bb26db08b66d2baad6464f8439d19 +86b533813321dbb180080910f233bcc4dd784fb21871aef41be08b7bfad4ecc3b68f228cb531 +7ac6ec1227bc7d0e452037ba918ee1da9fdb8393ae93b1e937a8d4691a17871d5092d2384b61 +90a53df888f65b951b05ed4ad57fe4b0c6a47b5b22f32a7f23c1a234c9feb5d8713d94968676 +0680da4db454f4acad972470033472b9864d63e8d23eefc87ebcf464ecf33f67fbcdd48eab38 +c5292586b36aef5981ed2fa07b2f9e23fc57d9eb71bfff4111c857e9fff23ceb31e72592e70c +874b4936""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid in precisely + # one byte + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x00\x01') + for val in dec[2:-10]: + self.assertNotEqual(val, 0) + self.assertEqual(dec[-10:], b'\x00forty two') + + plaintext = a2b_hex("c6ae80ffa80bc184b0") + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'forty two') + self.assertEqual(msg, plaintext) + + def test_negative_9_bytes_long_null_byte_in_first_byte_of_padding(self): + # an otherwise correct plaintext, but with wrong third byte + # (0x00 instead of non-zero), generates a random 9 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +8542c626fe533467acffcd4e617692244c9b5a3bf0a215c5d64891ced4bf4f9591b4b2aedff9 +843057986d81631b0acb3704ec2180e5696e8bd15b217a0ec36d2061b0e2182faa3d1c59bd3f +9086a10077a3337a3f5da503ec3753535ffd25b837a12f2541afefd0cffb0224b8f874e4bed1 +3949e105c075ed44e287c5ae03b155e06b90ed247d2c07f1ef3323e3508cce4e4074606c5417 +2ad74d12f8c3a47f654ad671104bf7681e5b061862747d9afd37e07d8e0e2291e01f14a95a1b +b4cbb47c304ef067595a3947ee2d722067e38a0f046f43ec29cac6a8801c6e3e9a2331b1d45a +7aa2c6af3205be382dd026e389614ee095665a611ab2e8dced2ee1c9d08ac9de11aef5b3803f +c9a9ce8231ec87b5fed386fb92ee3db995a89307bcba844bd0a691c29ae51216e949dfc81313 +3cb06a07265fd807bcb3377f6adb0a481d9b7f442003115895939773e6b95371c4febef29eda +e946fa245e7c50729e2e558cfaad773d1fd5f67b457a6d9d17a847c6fcbdb103a86f35f228ce +fc06cea0""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid in precisely + # one byte + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:3], b'\x00\x02\x00') + for val in dec[3:-10]: + self.assertNotEqual(val, 0) + self.assertEqual(dec[-10:], b'\x00forty two') + + plaintext = a2b_hex("a8a9301daa01bb25c7") + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'forty two') + self.assertEqual(msg, plaintext) + + def test_negative_9_bytes_long_null_byte_in_eighth_byte_of_padding(self): + # an otherwise correct plaintext, but with wrong tenth byte + # (0x00 instead of non-zero), generates a random 9 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +449dfa237a70a99cb0351793ec8677882021c2aa743580bf6a0ea672055cffe8303ac42855b1 +d1f3373aae6af09cb9074180fc963e9d1478a4f98b3b4861d3e7f0aa8560cf603711f139db77 +667ca14ba3a1acdedfca9ef4603d6d7eb0645bfc805304f9ad9d77d34762ce5cd84bd3ec9d35 +c30e3be72a1e8d355d5674a141b5530659ad64ebb6082e6f73a80832ab6388912538914654d3 +4602f4b3b1c78589b4a5d964b2efcca1dc7004c41f6cafcb5a7159a7fc7c0398604d0edbd4c8 +f4f04067da6a153a05e7cbeea13b5ee412400ef7d4f3106f4798da707ec37a11286df2b7a204 +856d5ff773613fd1e453a7114b78e347d3e8078e1cb3276b3562486ba630bf719697e0073a12 +3c3e60ebb5c7a1ccff4279faffa2402bc1109f8d559d6766e73591943dfcf25ba10c3762f02a +f85187799b8b4b135c3990793a6fd32642f1557405ba55cc7cf7336a0e967073c5fa50743f9c +c5e3017c172d9898d2af83345e71b3e0c22ab791eacb6484a32ec60ebc226ec9deaee91b1a05 +60c2b571""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid in precisely + # one byte + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x00\x02') + for val in dec[2:9]: + self.assertNotEqual(val, 0) + self.assertEqual(dec[9], 0) + for val in dec[10:-10]: + self.assertNotEqual(val, 0) + self.assertEqual(dec[-10:], b'\x00forty two') + + plaintext = a2b_hex("6c716fe01d44398018") + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'forty two') + self.assertEqual(msg, plaintext) + + def test_negative_9_bytes_long_missing_null_separator(self): + # an otherwise correct plaintext, but with the null byte specifying + # end of padding missing, generates a random 9 byte long plaintext + ciphertext = a2b_hex(remove_whitespace(""" +a7a5c99e50da48769ecb779d9abe86ef9ec8c38c6f43f17c7f2d7af608a4a1bd6cf695b47e97 +c191c61fb5a27318d02f495a176b9fae5a55b5d3fabd1d8aae4957e3879cb0c60f037724e11b +e5f30f08fc51c033731f14b44b414d11278cd3dba7e1c8bfe208d2b2bb7ec36366dacb6c88b2 +4cd79ab394adf19dbbc21dfa5788bacbadc6a62f79cf54fd8cf585c615b5c0eb94c35aa9de25 +321c8ffefb8916bbaa2697cb2dd82ee98939df9b6704cee77793edd2b4947d82e00e57496649 +70736c59a84197bd72b5c71e36aae29cd39af6ac73a368edbc1ca792e1309f442aafcd77c992 +c88f8e4863149f221695cb7b0236e75b2339a02c4ea114854372c306b9412d8eedb600a31532 +002f2cea07b4df963a093185e4607732e46d753b540974fb5a5c3f9432df22e85bb176113709 +66c5522fd23f2ad3484341ba7fd8885fc8e6d379a611d13a2aca784fba2073208faad2137bf1 +979a0fa146c1880d4337db3274269493bab44a1bcd0681f7227ffdf589c2e925ed9d36302509 +d1109ba4""")) + self.assertEqual(len(ciphertext), numBytes(self.pub_key.n)) + + # sanity check that the decrypted ciphertext is invalid in precisely + # one byte + dec = self.priv_key._raw_private_key_op_bytes(ciphertext) + self.assertEqual(dec[0:2], b'\x00\x02') + for val in dec[2:-10]: + self.assertNotEqual(val, 0) + self.assertEqual(dec[-10:], b'\x01forty two') + + plaintext = a2b_hex("aa2de6cde4e2442884") + + self.assertEqual(len(plaintext), 9) + + msg = self.priv_key.decrypt(ciphertext) + + self.assertNotEqual(msg, b'forty two') + self.assertEqual(msg, plaintext)