From: Sergey Matveev Date: Fri, 24 Jul 2020 16:22:20 +0000 (+0300) Subject: MGM mode X-Git-Tag: 4.8^0 X-Git-Url: http://www.git.cypherpunks.ru/?p=pygost.git;a=commitdiff_plain;h=6ee520badcc5298237f6a8a65e8cb0f749a980e1 MGM mode --- diff --git a/README b/README index 5a5035b..24b2808 100644 --- a/README +++ b/README @@ -23,6 +23,7 @@ GOST is GOvernment STandard of Russian Federation (and Soviet Union). * GOST R 34.12-2015 64-bit block cipher Магма (Magma) * GOST R 34.13-2015 padding methods and block cipher modes of operation (ECB, CTR, OFB, CBC, CFB, MAC) +* MGM AEAD mode for 64 and 128 bit ciphers * PEP247-compatible hash/MAC functions Known problems: low performance and non time-constant calculations. diff --git a/VERSION b/VERSION index 4f8c639..ef216a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.7 +4.8 diff --git a/install.texi b/install.texi index edfa486..2a518a7 100644 --- a/install.texi +++ b/install.texi @@ -1,7 +1,7 @@ @node Download @unnumbered Download -@set VERSION 4.7 +@set VERSION 4.8 No additional dependencies except Python 2.7/3.x interpreter are required. diff --git a/news.texi b/news.texi index f8c0e82..cccd4f3 100644 --- a/news.texi +++ b/news.texi @@ -3,6 +3,10 @@ @table @strong +@anchor{Release 4.8} +@item 4.8 +MGM AEAD mode for 64 and 128 bit ciphers. + @anchor{Release 4.7} @item 4.7 Removed @code{gost28147.addmod} for simplicity. diff --git a/pygost/__init__.py b/pygost/__init__.py index 4d1c86a..488683d 100644 --- a/pygost/__init__.py +++ b/pygost/__init__.py @@ -3,4 +3,4 @@ PyGOST is free software: see the file COPYING for copying conditions. """ -__version__ = "4.7" +__version__ = "4.8" diff --git a/pygost/mgm.py b/pygost/mgm.py new file mode 100644 index 0000000..0321628 --- /dev/null +++ b/pygost/mgm.py @@ -0,0 +1,168 @@ +# coding: utf-8 +# PyGOST -- Pure Python GOST cryptographic functions library +# Copyright (C) 2015-2020 Sergey Matveev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" Multilinear Galois Mode (MGM) block cipher mode. +""" + +from hmac import compare_digest + +from pygost.gost3413 import pad1 +from pygost.utils import bytes2long +from pygost.utils import long2bytes +from pygost.utils import strxor + + +def _incr(data, bs): + return long2bytes(bytes2long(data) + 1, size=bs // 2) + + +def incr_r(data, bs): + return data[:bs // 2] + _incr(data[bs // 2:], bs) + + +def incr_l(data, bs): + return _incr(data[:bs // 2], bs) + data[bs // 2:] + + +def nonce_prepare(nonce): + """Prepare nonce for MGM usage + + It just clears MSB. + """ + n = bytearray(nonce) + n[0] &= 0x7F + return bytes(n) + + +class MGM(object): + # Implementation is fully based on go.cypherpunks.ru/gogost/mgm + def __init__(self, encrypter, bs, tag_size=None): + """Multilinear Galois Mode (MGM) block cipher mode + + :param encrypter: encrypting function, that takes block as an input + :param int bs: cipher's blocksize + :param int tag_size: authentication tag size + (defaults to blocksize if not specified) + """ + if bs not in (8, 16): + raise ValueError("only 64/128-bit blocksizes allowed") + self.tag_size = bs if tag_size is None else bs + if self.tag_size < 4 or self.tag_size > bs: + raise ValueError("invalid tag_size") + self.encrypter = encrypter + self.bs = bs + self.max_size = (1 << (bs * 8 // 2)) - 1 + self.r = 0x1B if bs == 8 else 0x87 + + def _validate_nonce(self, nonce): + if len(nonce) != self.bs: + raise ValueError("nonce length must be equal to cipher's blocksize") + if bytearray(nonce)[0] & 0x80 > 0: + raise ValueError("nonce must not have higher bit set") + + def _validate_sizes(self, plaintext, additional_data): + if len(plaintext) == 0 and len(additional_data) == 0: + raise ValueError("at least one of plaintext or additional_data required") + if len(plaintext) + len(additional_data) > self.max_size: + raise ValueError("plaintext+additional_data are too big") + + def _mul(self, x, y): + x = bytes2long(x) + y = bytes2long(y) + z = 0 + max_bit = 1 << (self.bs * 8 - 1) + while y > 0: + if y & 1 == 1: + z ^= x + if x & max_bit > 0: + x = ((x ^ max_bit) << 1) ^ self.r + else: + x <<= 1 + y >>= 1 + return long2bytes(z, size=self.bs) + + def _crypt(self, icn, data): + icn[0] &= 0x7F + enc = self.encrypter(bytes(icn)) + res = [] + while len(data) > 0: + res.append(strxor(self.encrypter(enc), data)) + enc = incr_r(enc, self.bs) + data = data[self.bs:] + return b"".join(res) + + def _auth(self, icn, text, ad): + icn[0] |= 0x80 + enc = self.encrypter(bytes(icn)) + _sum = self.bs * b"\x00" + ad_len = len(ad) + text_len = len(text) + while len(ad) > 0: + _sum = strxor(_sum, self._mul( + self.encrypter(enc), + pad1(ad[:self.bs], self.bs), + )) + enc = incr_l(enc, self.bs) + ad = ad[self.bs:] + while len(text) > 0: + _sum = strxor(_sum, self._mul( + self.encrypter(enc), + pad1(text[:self.bs], self.bs), + )) + enc = incr_l(enc, self.bs) + text = text[self.bs:] + _sum = strxor(_sum, self._mul(self.encrypter(enc), ( + long2bytes(ad_len * 8, size=self.bs // 2) + + long2bytes(text_len * 8, size=self.bs // 2) + ))) + return self.encrypter(_sum)[:self.tag_size] + + def seal(self, nonce, plaintext, additional_data): + """Seal plaintext + + :param bytes nonce: blocksize-sized nonce. + Assure that it does not have MSB bit set + (:py:func:`pygost.mgm.nonce_prepare` helps) + :param bytes plaintext: plaintext to be encrypted and authenticated + :param bytes additional_data: additional data to be authenticated + """ + self._validate_nonce(nonce) + self._validate_sizes(plaintext, additional_data) + icn = bytearray(nonce) + ciphertext = self._crypt(icn, plaintext) + tag = self._auth(icn, ciphertext, additional_data) + return ciphertext + tag + + def open(self, nonce, ciphertext, additional_data): + """Open ciphertext + + :param bytes nonce: blocksize-sized nonce. + Assure that it does not have MSB bit set + (:py:func:`pygost.mgm.nonce_prepare` helps) + :param bytes ciphertext: ciphertext to be decrypted and authenticated + :param bytes additional_data: additional data to be authenticated + :raises ValueError: if ciphertext authentication fails + """ + self._validate_nonce(nonce) + self._validate_sizes(ciphertext, additional_data) + icn = bytearray(nonce) + ciphertext, tag_expected = ( + ciphertext[:-self.tag_size], + ciphertext[-self.tag_size:], + ) + tag = self._auth(icn, ciphertext, additional_data) + if not compare_digest(tag_expected, tag): + raise ValueError("invalid authentication tag") + return self._crypt(icn, ciphertext) diff --git a/pygost/stubs/pygost/mgm.pyi b/pygost/stubs/pygost/mgm.pyi new file mode 100644 index 0000000..81906b7 --- /dev/null +++ b/pygost/stubs/pygost/mgm.pyi @@ -0,0 +1,17 @@ +from typing import Callable + + +def nonce_prepare(nonce: bytes) -> bytes: ... + + +class MGM(object): + def __init__( + self, + encrypter: Callable[[bytes], bytes], + bs: int, + tag_size: int = None, + ) -> None: ... + + def seal(self, nonce: bytes, plaintext: bytes, additional_data: bytes) -> bytes: ... + + def open(self, nonce: bytes, ciphertext: bytes, additional_data: bytes) -> bytes: ... diff --git a/pygost/test.do b/pygost/test.do index e4fd871..06b9f97 100644 --- a/pygost/test.do +++ b/pygost/test.do @@ -1,13 +1,19 @@ -PYTHON=${PYTHON:-python} -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost28147 -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost28147_mac -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost341194 -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost34112012 -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost3410 -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost3410_vko -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_wrap -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost3412 -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost3413 -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_x509 -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_cms -PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_pfx +mods=" +gost28147 +gost28147_mac +gost341194 +gost34112012 +gost3410 +gost3410_vko +wrap +gost3412 +gost3413 +mgm +x509 +cms +pfx +" + +for mod in $mods ; do + PYTHONPATH=$PYTHONPATH:.. ${PYTHON:=python} -m unittest test_$mod +done diff --git a/pygost/test_mgm.py b/pygost/test_mgm.py new file mode 100644 index 0000000..0d692b6 --- /dev/null +++ b/pygost/test_mgm.py @@ -0,0 +1,57 @@ +# coding: utf-8 +# PyGOST -- Pure Python GOST cryptographic functions library +# Copyright (C) 2015-2020 Sergey Matveev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from os import urandom +from random import randint +from unittest import TestCase + +from pygost.gost3412 import GOST3412Magma +from pygost.gost3412 import GOST3412Kuznechik +from pygost.mgm import MGM +from pygost.mgm import nonce_prepare +from pygost.utils import hexdec + + +class TestVector(TestCase): + def runTest(self): + key = hexdec("8899AABBCCDDEEFF0011223344556677FEDCBA98765432100123456789ABCDEF") + ad = hexdec("0202020202020202010101010101010104040404040404040303030303030303EA0505050505050505") + plaintext = hexdec("1122334455667700FFEEDDCCBBAA998800112233445566778899AABBCCEEFF0A112233445566778899AABBCCEEFF0A002233445566778899AABBCCEEFF0A0011AABBCC") + mgm = MGM(GOST3412Kuznechik(key).encrypt, 16) + ciphertext = mgm.seal(plaintext[:16], plaintext, ad) + self.assertSequenceEqual(ciphertext[:len(plaintext)], hexdec("A9757B8147956E9055B8A33DE89F42FC8075D2212BF9FD5BD3F7069AADC16B39497AB15915A6BA85936B5D0EA9F6851CC60C14D4D3F883D0AB94420695C76DEB2C7552")) + self.assertSequenceEqual(ciphertext[len(plaintext):], hexdec("CF5D656F40C34F5C46E8BB0E29FCDB4C")) + self.assertSequenceEqual(mgm.open(plaintext[:16], ciphertext, ad), plaintext) + + +class TestSymmetric(TestCase): + def _itself(self, mgm, bs): + for _ in range(1000): + nonce = nonce_prepare(urandom(bs)) + ad = urandom(randint(0, 20)) + pt = urandom(randint(0, 20)) + if len(ad) + len(pt) == 0: + continue + ct = mgm.seal(nonce, pt, ad) + self.assertSequenceEqual(mgm.open(nonce, ct, ad), pt) + + def test_magma(self): + mgm = MGM(GOST3412Magma(urandom(32)).encrypt, 8) + self._itself(mgm, 8) + + def test_kuznechik(self): + mgm = MGM(GOST3412Kuznechik(urandom(32)).encrypt, 16) + self._itself(mgm, 16) diff --git a/www.texi b/www.texi index 9b4e79f..013780b 100644 --- a/www.texi +++ b/www.texi @@ -55,6 +55,7 @@ Currently supported algorithms are: @item GOST R 34.12-2015 64-bit block cipher Магма (Magma) @item GOST R 34.13-2015 padding methods and block cipher modes of operation (ECB, CTR, OFB, CBC, CFB, MAC) +@item MGM AEAD mode for 64 and 128 bit ciphers @item PEP247-compatible hash/MAC functions @end itemize