Skip to content
This repository was archived by the owner on Apr 20, 2025. It is now read-only.

PKCS#1 2.0: Implementation of MGF1 #89

Merged
merged 1 commit into from
Jun 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rsa/pkcs1.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def sign_hash(hash_value, priv_key, hash_method):

Hashes the message, then signs the hash with the given key. This is known
as a "detached signature", because the message itself isn't altered.

:param hash_value: A precomputed hash to sign (ignores message). Should be set to
None if needing to hash and sign message.
:param priv_key: the :py:class:`rsa.PrivateKey` to sign with
Expand Down
111 changes: 111 additions & 0 deletions rsa/pkcs1_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
#
# Copyright 2011 Sybren A. Stüvel <[email protected]>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Functions for PKCS#1 version 2 encryption and signing

This module implements certain functionality from PKCS#1 version 2. Main
documentation is RFC 2437: https://tools.ietf.org/html/rfc2437
"""

from rsa._compat import range
from rsa import (
common,
pkcs1,
transform,
)

HASH_METHOD_TO_BYTE_LENGTH = {
'MD5': 16,
'SHA-1': 20,
'SHA-256': 28,
'SHA-384': 48,
'SHA-512': 64,
}


def mgf1(seed, length, hasher='SHA-1'):
"""
MGF1 is a Mask Generation Function based on a hash function.

A mask generation function takes an octet string of variable length and a
desired output length as input, and outputs an octet string of the desired
length. The plaintext-awareness of RSAES-OAEP relies on the random nature of
the output of the mask generation function, which in turn relies on the
random nature of the underlying hash.

:param bytes seed: seed from which mask is generated, an octet string
:param int length: intended length in octets of the mask, at most 2^32(hLen)
:param str hasher: hash function (hLen denotes the length in octets of the hash
function output)

:return: mask, an octet string of length `length`
:rtype: bytes

:raise OverflowError: when `length` is too large for the specified `hasher`
:raise ValueError: when specified `hasher` is invalid
"""

try:
hash_length = HASH_METHOD_TO_BYTE_LENGTH[hasher]
except KeyError:
raise ValueError(
'Invalid `hasher` specified. Please select one of: {hash_list}'.format(
hash_list=', '.join(sorted(HASH_METHOD_TO_BYTE_LENGTH.keys()))
)
)

# If l > 2^32(hLen), output "mask too long" and stop.
if length > (2**32 * hash_length):
raise OverflowError(
"Desired length should be at most 2**32 times the hasher's output "
"length ({hash_length} for {hasher} function)".format(
hash_length=hash_length,
hasher=hasher,
)
)

# Looping `counter` from 0 to ceil(l / hLen)-1, build `output` based on the
# hashes formed by (`seed` + C), being `C` an octet string of length 4
# generated by converting `counter` with the primitive I2OSP
output = b''.join(
pkcs1.compute_hash(
seed + transform.int2bytes(counter, fill_size=4),
method_name=hasher,
)
for counter in range(common.ceil_div(length, hash_length) + 1)
)

# Output the leading `length` octets of `output` as the octet string mask.
return output[:length]


__all__ = [
'mgf1',
]

if __name__ == '__main__':
print('Running doctests 1000x or until failure')
import doctest

for count in range(1000):
(failures, tests) = doctest.testmod()
if failures:
break

if count and count % 100 == 0:
print('%i times' % count)

print('Doctests done')
83 changes: 83 additions & 0 deletions tests/test_pkcs1_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
#
# Copyright 2011 Sybren A. Stüvel <[email protected]>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests PKCS #1 version 2 functionality.

Most of the mocked values come from the test vectors found at:
http://www.itomorrowmag.com/emc-plus/rsa-labs/standards-initiatives/pkcs-rsa-cryptography-standard.htm
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

"""

import unittest

from rsa import pkcs1_v2


class MGFTest(unittest.TestCase):
def test_oaep_int_db_mask(self):
seed = (
b'\xaa\xfd\x12\xf6\x59\xca\xe6\x34\x89\xb4\x79\xe5\x07\x6d\xde\xc2'
b'\xf0\x6c\xb5\x8f'
)
db = (
b'\xda\x39\xa3\xee\x5e\x6b\x4b\x0d\x32\x55\xbf\xef\x95\x60\x18\x90'
b'\xaf\xd8\x07\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xd4\x36\xe9\x95\x69'
b'\xfd\x32\xa7\xc8\xa0\x5b\xbc\x90\xd3\x2c\x49'
)
masked_db = (
b'\xdc\xd8\x7d\x5c\x68\xf1\xee\xa8\xf5\x52\x67\xc3\x1b\x2e\x8b\xb4'
b'\x25\x1f\x84\xd7\xe0\xb2\xc0\x46\x26\xf5\xaf\xf9\x3e\xdc\xfb\x25'
b'\xc9\xc2\xb3\xff\x8a\xe1\x0e\x83\x9a\x2d\xdb\x4c\xdc\xfe\x4f\xf4'
b'\x77\x28\xb4\xa1\xb7\xc1\x36\x2b\xaa\xd2\x9a\xb4\x8d\x28\x69\xd5'
b'\x02\x41\x21\x43\x58\x11\x59\x1b\xe3\x92\xf9\x82\xfb\x3e\x87\xd0'
b'\x95\xae\xb4\x04\x48\xdb\x97\x2f\x3a\xc1\x4f\x7b\xc2\x75\x19\x52'
b'\x81\xce\x32\xd2\xf1\xb7\x6d\x4d\x35\x3e\x2d'
)

# dbMask = MGF(seed, length(DB))
db_mask = pkcs1_v2.mgf1(seed, length=len(db))
expected_db_mask = (
b'\x06\xe1\xde\xb2\x36\x9a\xa5\xa5\xc7\x07\xd8\x2c\x8e\x4e\x93\x24'
b'\x8a\xc7\x83\xde\xe0\xb2\xc0\x46\x26\xf5\xaf\xf9\x3e\xdc\xfb\x25'
b'\xc9\xc2\xb3\xff\x8a\xe1\x0e\x83\x9a\x2d\xdb\x4c\xdc\xfe\x4f\xf4'
b'\x77\x28\xb4\xa1\xb7\xc1\x36\x2b\xaa\xd2\x9a\xb4\x8d\x28\x69\xd5'
b'\x02\x41\x21\x43\x58\x11\x59\x1b\xe3\x92\xf9\x82\xfb\x3e\x87\xd0'
b'\x95\xae\xb4\x04\x48\xdb\x97\x2f\x3a\xc1\x4e\xaf\xf4\x9c\x8c\x3b'
b'\x7c\xfc\x95\x1a\x51\xec\xd1\xdd\xe6\x12\x64'
)

self.assertEqual(db_mask, expected_db_mask)

# seedMask = MGF(maskedDB, length(seed))
seed_mask = pkcs1_v2.mgf1(masked_db, length=len(seed))
expected_seed_mask = (
b'\x41\x87\x0b\x5a\xb0\x29\xe6\x57\xd9\x57\x50\xb5\x4c\x28\x3c\x08'
b'\x72\x5d\xbe\xa9'
)

self.assertEqual(seed_mask, expected_seed_mask)

def test_invalid_hasher(self):
"""Tests an invalid hasher generates an exception"""
with self.assertRaises(ValueError):
pkcs1_v2.mgf1(b'\x06\xe1\xde\xb2', length=8, hasher='SHA2')

def test_invalid_length(self):
with self.assertRaises(OverflowError):
pkcs1_v2.mgf1(b'\x06\xe1\xde\xb2', length=2**50)