Arthur de Jong

Open Source / Free Software developer

summaryrefslogtreecommitdiffstats
path: root/pskc/mac.py
blob: b4ddd53b32c578975151fb9f204e51086a3e802f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# mac.py - module for checking value signatures
# coding: utf-8
#
# Copyright (C) 2014-2016 Arthur de Jong
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""Module that provides message authentication for PSKC values.

This module provides a MAC class that is used to store information about
how the MAC should be calculated (including the MAC key) and a ValueMAC
class that provides (H)MAC checking for PSKC key data.

The MAC key is generated specifically for each PSKC file and encrypted
with the PSKC encryption key.
"""


import base64
import re


_hmac_url_re = re.compile(r'^.*#hmac-(?P<hash>[a-z0-9]+)$')


def get_hash(algorithm):
    """Return the hash function for the specifies HMAC algorithm."""
    import hashlib
    match = _hmac_url_re.search(algorithm)
    if match:
        return getattr(hashlib, match.group('hash'), None)


def get_hmac(algorithm):
    """Return an HMAC function that takes a secret and a value and returns a
    digest."""
    import hmac
    digestmod = get_hash(algorithm)
    if digestmod is not None:
        return lambda key, value: hmac.new(key, value, digestmod).digest()


class MAC(object):
    """Class describing the MAC algorithm to use and how to get the key.

    Instances of this class provide the following attributes:

      algorithm: the name of the HMAC to use (currently only HMAC_SHA1)
      key: the binary value of the MAC key if it can be decrypted
    """

    def __init__(self, pskc):
        self.pskc = pskc
        self._algorithm = None
        self.key_plain_value = None
        self.key_cipher_value = None
        self.key_algorithm = None

    def parse(self, mac_method):
        """Read MAC information from the <MACMethod> XML tree."""
        from pskc.xml import find, findtext, findbin
        if mac_method is None:
            return
        self.algorithm = mac_method.get('Algorithm')
        mac_key = find(mac_method, 'MACKey')
        if mac_key is not None:
            self.key_cipher_value = findbin(mac_key, 'CipherData/CipherValue')
            encryption_method = find(mac_key, 'EncryptionMethod')
            if encryption_method is not None:
                self.key_algorithm = encryption_method.attrib.get('Algorithm')
        mac_key_reference = findtext(mac_method, 'MACKeyReference')

    def make_xml(self, container):
        from pskc.xml import mk_elem
        if not self.algorithm and not self.key:
            return
        mac_method = mk_elem(
            container, 'pskc:MACMethod', Algorithm=self.algorithm, empty=True)
        mac_key = mk_elem(mac_method, 'pskc:MACKey', empty=True)
        mk_elem(
            mac_key, 'xenc:EncryptionMethod',
            Algorithm=self.pskc.encryption.algorithm)
        cipher_data = mk_elem(mac_key, 'xenc:CipherData', empty=True)
        if self.key_cipher_value:
            mk_elem(
                cipher_data, 'xenc:CipherValue',
                base64.b64encode(self.key_cipher_value).decode())
        elif self.key_plain_value:
            mk_elem(
                cipher_data, 'xenc:CipherValue', base64.b64encode(
                    self.pskc.encryption.encrypt_value(self.key_plain_value)
                ).decode())

    @property
    def key(self):
        """Provides access to the MAC key binary value if available."""
        if self.key_plain_value:
            return self.key_plain_value
        elif self.key_cipher_value:
            return self.pskc.encryption.decrypt_value(
                self.key_cipher_value, self.key_algorithm)

    @key.setter
    def key(self, value):
        self.key_plain_value = value
        self.key_cipher_value = None

    @property
    def algorithm(self):
        """Provide the MAC algorithm used."""
        if self._algorithm:
            return self._algorithm

    @algorithm.setter
    def algorithm(self, value):
        from pskc.encryption import normalise_algorithm
        self._algorithm = normalise_algorithm(value)

    @property
    def algorithm_key_length(self):
        """Recommended minimal key length in bytes for the set algorithm."""
        # https://tools.ietf.org/html/rfc2104#section-3
        # an HMAC key should be at least as long as the hash output length
        hashfn = get_hash(self.algorithm)
        if hashfn is not None:
            return int(hashfn().digest_size)
        else:
            return 16

    def generate_mac(self, value):
        """Generate the MAC over the specified value."""
        from pskc.exceptions import DecryptionError
        key = self.key
        if key is None:
            raise DecryptionError('No MAC key available')
        hmacfn = get_hmac(self.algorithm)
        if hmacfn is None:
            raise DecryptionError(
                'Unsupported MAC algorithm: %r' % self.algorithm)
        return hmacfn(key, value)

    def check_value(self, value, value_mac):
        """Check if the provided value matches the MAC.

        This will return None if there is no MAC to be checked. It will
        return True if the MAC matches and raise an exception if it fails.
        """
        from pskc.exceptions import DecryptionError
        if self.generate_mac(value) != value_mac:
            raise DecryptionError('MAC value does not match')
        return True

    def setup(self, key=None, algorithm=None):
        """Configure an encrypted MAC key.

        The following arguments may be supplied:
          key: the MAC key to use
          algorithm: MAC algorithm

        None of the arguments are required, reasonable defaults will be
        chosen for missing arguments.
        """
        if key:
            self.key = key
        if algorithm:
            self.algorithm = algorithm
        # default to HMAC-SHA1
        if not self.algorithm:
            self.algorithm = 'hmac-sha1'
        # generate an HMAC key
        if not self.key:
            from Crypto import Random
            self.key = Random.get_random_bytes(self.algorithm_key_length)