|
11 | 11 |
|
12 | 12 | import io
|
13 | 13 | import os
|
| 14 | +import re |
14 | 15 | import sys
|
15 | 16 | import socket
|
16 | 17 | import struct
|
@@ -734,30 +735,93 @@ def PipeClient(address):
|
734 | 735 | WELCOME = b'#WELCOME#'
|
735 | 736 | FAILURE = b'#FAILURE#'
|
736 | 737 |
|
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 | + """ |
738 | 782 | 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): |
739 | 801 | if not isinstance(authkey, bytes):
|
740 | 802 | raise ValueError(
|
741 | 803 | "Authkey must be bytes, not {0!s}".format(type(authkey)))
|
742 | 804 | message = os.urandom(MESSAGE_LENGTH)
|
| 805 | + if digestmod is not None: |
| 806 | + message = b'{%s}%s' % (digestmod.encode('ascii'), message) |
743 | 807 | connection.send_bytes(CHALLENGE + message)
|
744 |
| - digest = hmac.new(authkey, message, 'md5').digest() |
745 | 808 | 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: |
749 | 812 | connection.send_bytes(FAILURE)
|
750 |
| - raise AuthenticationError('digest received was wrong') |
| 813 | + raise |
| 814 | + else: |
| 815 | + connection.send_bytes(WELCOME) |
751 | 816 |
|
752 | 817 | def answer_challenge(connection, authkey):
|
753 |
| - import hmac |
754 | 818 | if not isinstance(authkey, bytes):
|
755 | 819 | raise ValueError(
|
756 | 820 | "Authkey must be bytes, not {0!s}".format(type(authkey)))
|
757 | 821 | message = connection.recv_bytes(256) # reject large message
|
758 | 822 | assert message[:len(CHALLENGE)] == CHALLENGE, 'message = %r' % message
|
759 | 823 | message = message[len(CHALLENGE):]
|
760 |
| - digest = hmac.new(authkey, message, 'md5').digest() |
| 824 | + digest = _create_response(authkey, message) |
761 | 825 | connection.send_bytes(digest)
|
762 | 826 | response = connection.recv_bytes(256) # reject large message
|
763 | 827 | if response != WELCOME:
|
|
0 commit comments