Skip to content

Commit c7f7680

Browse files
committed
bpo-17258: Stronger HMAC in multiprocessing
Signed-off-by: Christian Heimes <[email protected]>
1 parent f03d318 commit c7f7680

File tree

3 files changed

+103
-10
lines changed

3 files changed

+103
-10
lines changed

Lib/multiprocessing/connection.py

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import io
1313
import os
14+
import re
1415
import sys
1516
import socket
1617
import struct
@@ -734,30 +735,93 @@ def PipeClient(address):
734735
WELCOME = b'#WELCOME#'
735736
FAILURE = b'#FAILURE#'
736737

737-
def deliver_challenge(connection, authkey):
738+
_mac_algo_re = re.compile(
739+
rb'^{(?P<digestmod>(md5|sha256|sha384|sha3_256|sha3_384))}'
740+
rb'(?P<payload>.*)$'
741+
)
742+
743+
def _create_response(authkey, message):
744+
"""Create a MAC based on authkey and message
745+
746+
The MAC algorithm defaults to HMAC-MD5, unless MD5 is not available or
747+
the message has a '{digestmod}' prefix. For legacy HMAC-MD5, the response
748+
is the raw MAC, otherwise the response is prefixed with '{digestmod}',
749+
e.g. b'{sha256}abcdefg...'
750+
751+
Note: The MAC protects the entire message including the digestmod prefix.
752+
"""
753+
import hmac
754+
# message: {digest}payload, the MAC protects header and payload
755+
mo = _mac_algo_re.match(message)
756+
if mo is not None:
757+
digestmod = mo.group('digestmod').decode('ascii')
758+
else:
759+
# old-style MD5 with fallback
760+
digestmod = None
761+
762+
if digestmod is None:
763+
try:
764+
return hmac.new(authkey, message, 'md5').digest()
765+
except ValueError:
766+
# MD5 is not available, fall back to SHA2-256
767+
digestmod = 'sha256'
768+
prefix = b'{%s}' % digestmod.encode('ascii')
769+
return prefix + hmac.new(authkey, message, digestmod).digest()
770+
771+
772+
def _verify_challenge(authkey, message, response):
773+
"""Verify MAC challenge
774+
775+
If our message did not include a digestmod prefix, the client is allowed
776+
to select a stronger digestmod (HMAC-MD5 legacy to HMAC-SHA2-256).
777+
778+
In case our message is prefixed, a client cannot downgrade to a weaker
779+
algorithm, because the MAC is calculated over the entire message
780+
including the '{digestmod}' prefix.
781+
"""
738782
import hmac
783+
mo = _mac_algo_re.match(response)
784+
if mo is not None:
785+
# get digestmod from response.
786+
digestmod = mo.group('digestmod').decode('ascii')
787+
mac = mo.group('payload')
788+
else:
789+
digestmod = 'md5'
790+
mac = response
791+
try:
792+
expected = hmac.new(authkey, message, digestmod).digest()
793+
except ValueError:
794+
raise AuthenticationError(f'unsupported digest {digestmod}')
795+
if not hmac.compare_digest(expected, mac):
796+
raise AuthenticationError('digest received was wrong')
797+
return True
798+
799+
800+
def deliver_challenge(connection, authkey, digestmod=None):
739801
if not isinstance(authkey, bytes):
740802
raise ValueError(
741803
"Authkey must be bytes, not {0!s}".format(type(authkey)))
742804
message = os.urandom(MESSAGE_LENGTH)
805+
if digestmod is not None:
806+
message = b'{%s}%s' % (digestmod.encode('ascii'), message)
743807
connection.send_bytes(CHALLENGE + message)
744-
digest = hmac.new(authkey, message, 'md5').digest()
745808
response = connection.recv_bytes(256) # reject large message
746-
if response == digest:
747-
connection.send_bytes(WELCOME)
748-
else:
809+
try:
810+
_verify_challenge(authkey, message, response)
811+
except AuthenticationError:
749812
connection.send_bytes(FAILURE)
750-
raise AuthenticationError('digest received was wrong')
813+
raise
814+
else:
815+
connection.send_bytes(WELCOME)
751816

752817
def answer_challenge(connection, authkey):
753-
import hmac
754818
if not isinstance(authkey, bytes):
755819
raise ValueError(
756820
"Authkey must be bytes, not {0!s}".format(type(authkey)))
757821
message = connection.recv_bytes(256) # reject large message
758822
assert message[:len(CHALLENGE)] == CHALLENGE, 'message = %r' % message
759823
message = message[len(CHALLENGE):]
760-
digest = hmac.new(authkey, message, 'md5').digest()
824+
digest = _create_response(authkey, message)
761825
connection.send_bytes(digest)
762826
response = connection.recv_bytes(256) # reject large message
763827
if response != WELCOME:

Lib/test/_test_multiprocessing.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import multiprocessing.managers
4747
import multiprocessing.pool
4848
import multiprocessing.queues
49+
from multiprocessing.connection import wait, AuthenticationError
4950

5051
from multiprocessing import util
5152

@@ -118,8 +119,6 @@ def _resource_unlink(name, rtype):
118119

119120
WIN32 = (sys.platform == "win32")
120121

121-
from multiprocessing.connection import wait
122-
123122
def wait_for_handle(handle, timeout):
124123
if timeout is not None and timeout < 0.0:
125124
timeout = None
@@ -4494,6 +4493,35 @@ def send_bytes(self, data):
44944493
multiprocessing.connection.answer_challenge,
44954494
_FakeConnection(), b'abc')
44964495

4496+
4497+
@hashlib_helper.requires_hashdigest('md5')
4498+
@hashlib_helper.requires_hashdigest('sha256')
4499+
class ChallengeResponseTest(unittest.TestCase):
4500+
authkey = b'supadupasecretkey'
4501+
4502+
def create_response(self, message):
4503+
return multiprocessing.connection._create_response(
4504+
self.authkey, message
4505+
)
4506+
4507+
def verify_challenge(self, message, response):
4508+
return multiprocessing.connection._verify_challenge(
4509+
self.authkey, message, response
4510+
)
4511+
4512+
def test_challengeresponse(self):
4513+
for algo in [None, "md5", "sha256"]:
4514+
msg = b'mymessage'
4515+
if algo is not None:
4516+
prefix = b'{%s}' % algo.encode("ascii")
4517+
else:
4518+
prefix = b''
4519+
msg = prefix + msg
4520+
response = self.create_response(msg)
4521+
if not response.startswith(prefix):
4522+
self.fail(response)
4523+
self.verify_challenge(msg, response)
4524+
44974525
#
44984526
# Test Manager.start()/Pool.__init__() initializer feature - see issue 5585
44994527
#
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:mod:`multiprocessing` supports stronger HMAC algorithms

0 commit comments

Comments
 (0)