[Git][NTPsec/ntpsec][master] 4 commits: Auth: Fix auth for modes 4 & 6 ...

Hal Murray gitlab at mg.gitlab.com
Sat May 16 09:03:58 UTC 2020



Hal Murray pushed to branch master at NTPsec / ntpsec


Commits:
798b931d by James Browning at 2020-05-16T08:53:33+00:00
Auth: Fix auth for modes 4 & 6 ...

but only if you can provide
- the point the packet ends
- the point the mac begins

this is usually 48 for most modes
- mode 0 is reserved
- modes 1, 2, 5 and 7 are no longer supported
- mode 3 is the servers problem
- mode 4 could be a problem w/ notional extensions IMO
- mode 6 seems taken care of

Also,
- Shorten long macs to work with existing code
- Dehexify long passwords
- Add some function docstrings
- Remove some per packet authentication messages
- change over to cryptography from ntp.ntpc 
- truncate client keys to 256b 32B
- String mangling
- auto-pep8ify
- - - - -
f1c021ff by James Browning at 2020-05-16T08:53:33+00:00
add shallow mockery of ntpkeygen
- - - - -
0a981001 by James Browning at 2020-05-16T08:53:33+00:00
ntpq: aes and output support fixes


- - - - -
cf99856a by James Browning at 2020-05-16T08:53:33+00:00
Update news


- - - - -


6 changed files:

- NEWS.adoc
- + contrib/keygone.py
- ntpclients/ntpdig.py
- ntpclients/ntpq.py
- pylib/packet.py
- tests/pylib/test_packet.py


Changes:

=====================================
NEWS.adoc
=====================================
@@ -12,6 +12,10 @@ on user-visible changes.
 
 == Repository head ==
 
+Change ntpq to display better and work with new ntp.ntpc authentication.
+
+Change over to cryptographic routines from ntp.ntp_control
+
 Remove support for NetInfo. NetInfo was last supported in Mac OS X v10.4
 
 The configure step now supports --disable-nts for running


=====================================
contrib/keygone.py
=====================================
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# SPDX-License-Identifier: BSD-2-Clause
+'''keygone.py -- A cruddy improvised NTPsec keyfile generator
+
+The number in the group_iterator range is the number of keys to generate
+per key type.
+
+key_number starts as the first key number and is incremented.
+
+max_bytes is the implementation-specific maximum number of entropy bytes
+allowed per key.
+
+types_lengths_list is a list of lists. the inner lists store the output
+length of the algorithm (block size?) and the standard form of the
+algorithms name (in lowercase). Commented out algorithms are believed
+broken, non-commented ones are not. This list believed accurate as of
+the 6th of May 2020.
+'''
+import secrets
+
+group_iterator = range(10)
+key_number = 100
+max_bytes = 32
+
+types_lengths_list = [
+    # [16, 'md5'],
+     [16, 'aes-128'],
+     [16, 'aria-128'],
+     [16, 'camellia-128'],
+     [16, 'sm4'],
+
+    # [20, 'sha1'],
+    # [20, 'rmd160'],
+
+     [24, 'aes-192'],
+     [24, 'aria-192'],
+     [24, 'camellia-192'],
+
+     [28, 'sha224'],
+     [28, 'sha3-224'],
+
+     [32, 'blake2s256'],
+     [32, 'sha256'],
+     [32, 'sha3-256'],
+     [32, 'aes-256'],
+     [32, 'aria-256'],
+     [32, 'camellia-256'],
+
+     [48, 'sha384'],
+     [48, 'sha3-384'],
+
+     [64, 'blake2b512'],
+     [64, 'sha512'],
+     [64, 'sha3-512']]
+
+for type_length_tuple in types_lengths_list:
+    length = min(type_length_tuple[0], max_bytes)
+    for _ in group_iterator:
+        print("%3d %12s\t%s" % 
+        (key_number, type_length_tuple[1],
+         secrets.token_hex(length)))
+        key_number += 1


=====================================
ntpclients/ntpdig.py
=====================================
@@ -63,7 +63,7 @@ def read_append(s, packets, packet, sockaddr):
         if not ntp.packet.Authenticator.have_mac(d):
             if debug:
                 log("no MAC on reply from %s" % packet.hostname)
-        if not credentials.verify_mac(d):
+        if not credentials.verify_mac(d, packet_end=48, mac_begin=48):
             packet.trusted = False
             log("MAC verification on reply from %s failed"
                 % sockaddr[0])


=====================================
ntpclients/ntpq.py
=====================================
@@ -752,15 +752,12 @@ usage: ntpversion [version number]
         "set key type to use for authenticated requests"
         if not line:
             self.say("Keytype: %s\n" % self.session.keytype)
-        elif line not in "DSA, MD4, MD5, MDC2, RIPEMD160, SHA-1, AES-CMAC":
-            # Above list is somewhat bogus. All but oldest versions of NTPsec
-            # will cheerfully use any 16- or 20-bit MAC supported by libcrypto;
-            # NTP Classic will probably barf on AES-CMAC.
-            self.warn("Keytype %s is not supported by ntpd.\n" % line)
-        elif line not in hashlib.algorithms_available:
-            self.warn("Keytype %s is not supported by ntpq.\n" % line)
+        elif line.upper() in ['AES', 'AES128CMAC']:
+            self.session.keytype = 'AES-128'
+        elif not ntp.ntpc.checkname(line.upper()):
+            self.warn("Keytype %s is not supported by openSSL or ntpq.\n" % line)
         else:
-            self.session.keytype = line
+            self.session.keytype = line.upper()
 
     def help_keytype(self):
         self.say("""\
@@ -1171,6 +1168,7 @@ usage: lopeers
             self.warn("In Config\nKeyword = :config\nCommand = %s" % line)
         try:
             self.session.config(line)
+            self.session.response = ntp.poly.polystr(self.session.response)
             m = re.match("column ([0-9]+) syntax error", self.session.response)
             if m:
                 col = int(m.group(1))


=====================================
pylib/packet.py
=====================================
@@ -205,7 +205,7 @@ A Mode 6 packet cannot have extension fields.
 # SPDX-License-Identifier: BSD-2-Clause
 from __future__ import print_function, division
 import getpass
-import hashlib
+import hmac
 import os
 import select
 import socket
@@ -253,6 +253,13 @@ DEFSTIMEOUT = 3000
 # The maximum keyid for authentication, keyid is a 16-bit field
 MAX_KEYID = 0xFFFF
 
+# Some constants (in Bytes) for mode6 and authentication
+MODE_SIX_HEADER_LENGTH = 12
+MINIMUM_MAC_LENGTH = 16
+KEYID_LENGTH = 4
+MODE_SIX_ALIGNMENT = 8
+MAX_BARE_MAC_LENGTH = 20
+
 
 class Packet:
     "Encapsulate an NTP fragment"
@@ -698,6 +705,7 @@ class ControlException(BaseException):
 class ControlSession:
     "A session to a host"
     MRU_ROW_LIMIT = 256
+    _authpass = True
     server_errors = {
         ntp.control.CERR_UNSPEC: "UNSPEC",
         ntp.control.CERR_PERMISSION: "PERMISSION",
@@ -1024,6 +1032,16 @@ class ControlSession:
                 continue
 
             # Someday, perhaps, check authentication here
+            if self._authpass and self.auth:
+                _pend = rpkt.count + MODE_SIX_HEADER_LENGTH
+                _pend += (-_pend % MODE_SIX_ALIGNMENT)
+                if len(rawdata) < (_pend + KEYID_LENGTH + MINIMUM_MAC_LENGTH):
+                    self.logfp.write('AUTH - packet too short for MAC %d < %d\n' %
+                                     (len(rawdata), (_pend + KEYID_LENGTH + MINIMUM_MAC_LENGTH)))
+                    self._authpass = False
+                elif not self.auth.verify_mac(rawdata, packet_end=_pend,
+                                            mac_begin=_pend):
+                    self._authpass = False
 
             # Clip off the MAC, if any
             rpkt.extension = rpkt.extension[:rpkt.count]
@@ -1108,6 +1126,8 @@ class ControlSession:
                         warn("First line:\n%s\n" % repr(firstline))
                     return None
                 break
+        if not self._authpass:
+            warn('AUTH: Content untrusted due to authentication failure!\n')
 
     def __validate_packet(self, rpkt, rawdata, opcode, associd):
         # TODO: refactor to simplify while retaining semantic info
@@ -1553,15 +1573,15 @@ def parse_mru_variables(variables):
             "addr": lambda e: e.sortaddr(),
             # IPv6 desc. then IPv4 desc.
             "-addr": lambda e: e.sortaddr(),
-            # hit count ascending 
+            # hit count ascending
             "count": lambda e: -e.ct,
             # hit count descending
             "-count": lambda e: e.ct,
-            # score ascending 
+            # score ascending
             "score": lambda e: -e.sc,
             # score descending
             "-score": lambda e: e.sc,
-            # drop count ascending 
+            # drop count ascending
             "drop": lambda e: -e.dr,
             # drop count descending
             "-drop": lambda e: e.dr,
@@ -1675,16 +1695,24 @@ class Authenticator:
                 if not line:
                     continue
                 (keyid, keytype, passwd) = line.split()
+                if keytype.upper() in ['AES', 'AES128CMAC']:
+                    keytype = 'AES-128'
+                if len(passwd) > 20:
+                    # if len(passwd) > 64:
+                        # print('AUTH: Truncating key %s to 256bits (32Bytes)' % keyid)
+                    passwd = ntp.util.hexstr2octets(passwd[:64])
                 self.passwords[int(keyid)] = (keytype, passwd)
 
     def __len__(self):
+        'return the number of keytype/passwd tuples stored'
         return len(self.passwords)
 
     def __getitem__(self, keyid):
+        'get a keytype/passwd tuple by keyid'
         return self.passwords.get(keyid)
 
     def control(self, keyid=None):
-        "Get a keyid/passwd pair that is trusted on localhost"
+        "Get the keytype/passwd tuple that controls localhost and its id"
         if keyid is not None:
             if keyid in self.passwords:
                 return (keyid,) + self.passwords[keyid]
@@ -1700,19 +1728,19 @@ class Authenticator:
                 if len(passwd) > 20:
                     passwd = ntp.util.hexstr2octets(passwd)
                 return (keyid, keytype, passwd)
-        else:
-            # No control lines found
-            raise ValueError
+        # No control lines found
+        raise ValueError
 
     @staticmethod
     def compute_mac(payload, keyid, keytype, passwd):
-        hasher = hashlib.new(keytype)
-        hasher.update(ntp.poly.polybytes(passwd))
-        hasher.update(payload)
-        if hasher.digest_size == 0:
-            return None
-        else:
-            return struct.pack("!I", keyid) + hasher.digest()
+        'Create the authentication payload to send'
+        if not ntp.ntpc.checkname(keytype):
+            return False
+        mac2 = ntp.ntpc.mac(ntp.poly.polybytes(payload),
+                            ntp.poly.polybytes(passwd), keytype)
+        if not mac2 or len(mac2) == 0:
+            return b''
+        return struct.pack("!I", keyid) + mac2
 
     @staticmethod
     def have_mac(packet):
@@ -1722,20 +1750,22 @@ class Authenticator:
         # On those you have to go in and look at the count.
         return len(packet) > ntp.magic.LEN_PKT_NOMAC
 
-    def verify_mac(self, packet):
+    def verify_mac(self, packet, packet_end=48, mac_begin=48):
         "Does the MAC on this packet verify according to credentials we have?"
-        # FIXME: Someday, figure out how to handle SHA1?
-        HASHLEN = 16    # Length of MD5 hash.
-        payload = packet[:-HASHLEN-4]
-        keyid = packet[-HASHLEN-4:-HASHLEN]
-        mac = packet[-HASHLEN:]
+        payload = packet[:packet_end]
+        keyid = packet[mac_begin:mac_begin+KEYID_LENGTH]
+        mac = packet[mac_begin+KEYID_LENGTH:]
         (keyid,) = struct.unpack("!I", keyid)
         if keyid not in self.passwords:
+            # print('AUTH: No key %08x...' % keyid)
             return False
         (keytype, passwd) = self.passwords[keyid]
-        hasher = hashlib.new(keytype)
-        hasher.update(passwd)
-        hasher.update(payload)
-        return ntp.poly.polybytes(hasher.digest()) == mac
+        if not ntp.ntpc.checkname(keytype):
+            return False
+        mac2 = ntp.ntpc.mac(ntp.poly.polybytes(payload),
+                            ntp.poly.polybytes(passwd), keytype)
+        if not mac2:
+            return False
+        return hmac.compare_digest(mac, mac2)
 
 # end


=====================================
tests/pylib/test_packet.py
=====================================
@@ -3,17 +3,17 @@
 
 from __future__ import print_function, division
 
-import unittest
-import ntp.packet
-import ntp.control
-import ntp.util
-import ntp.magic
-import socket
+import getpass
 import select
+import socket
 import sys
-import getpass
+import unittest
 import jigs
+import ntp.control
+import ntp.magic
+import ntp.packet
 import ntp.poly
+import ntp.util
 
 odict = ntp.util.OrderedDict
 
@@ -2082,17 +2082,13 @@ class TestAuthenticator(unittest.TestCase):
 
     def test_compute_mac(self):
         f = self.target.compute_mac
-        try:
-            temphash = ntpp.hashlib
-            fakehashlibmod = jigs.HashlibModuleJig()
-            ntpp.hashlib = fakehashlibmod
-            # Test no digest
-            self.assertEqual(f("", 0, None, ntp.poly.polybytes("")), None)
-            # Test with digest
-            self.assertEqual(f("foo", 0x42, "bar", "quux"),
-                             ntp.poly.polybytes("\x00\x00\x00\x42blahblahblahblah"))
-        finally:
-            ntpp.hashlib = temphash
+        pkt = ntp.util.hexstr2octets('240300e8000012ce0000091941138e89' +
+                                     'e25b102e9fe94dc9e25b1175bd5a3000' + 'e25b1175bd6cf48ee25b1175bd70e594')
+        mac1 = b'\x00\x00\x00\rL\x7f\xc1\xd1\xe9\xd3\xf8\xec\x91\xdf\xecS\x89e\xc5\xf3'
+        key1 = ntp.util.hexstr2octets('2f3badbb640bf975fec519df8a83e829')
+        key2 = ''
+        self.assertEqual(f(pkt, 0x0d, 'aes', key1), mac1)
+        self.assertEqual(f(pkt, 0x0e, 'neun', key2), False)
 
     def test_have_mac(self):
         f = self.target.have_mac
@@ -2105,19 +2101,20 @@ class TestAuthenticator(unittest.TestCase):
 
     def test_verify_mac(self):
         cls = self.target()
-        cls.passwords[0x23] = ("a", "z")
-        good_pkt = "foobar\x00\x00\x00\x23blahblahblahblah"
-        bad_pkt = "foobar\xDE\xAD\xDE\xAFblahblahblah"
-        try:
-            temphash = ntpp.hashlib
-            fakehashlibmod = jigs.HashlibModuleJig()
-            ntpp.hashlib = fakehashlibmod
-            # Test good
-            self.assertEqual(cls.verify_mac(ntp.poly.polybytes(good_pkt)), True)
-            # Test bad
-            self.assertEqual(cls.verify_mac(ntp.poly.polybytes(bad_pkt)), False)
-        finally:
-            ntpp.hashlib = temphash
+        cls.passwords[0x0d] = (
+            'aes-128', ntp.util.hexstr2octets('2f3badbb640bf975fec519df8a83e829'))
+        good_pkt = '240300e80000139a00000ae8cc0286a2' + 'e25c0c4dfff93ee2e25c0cca53f45000' + \
+            'e25c0cca54048d79e25c0cca5408646b' + \
+            '0000000dbe93e3f1d530d9252147c298' + 'c00c85f9'
+        bad_pkt = '240300e80000131f00000779cc0286a2' + 'e25c0d54ff6e4835e25c0dc2bea43000' + \
+            'e25c0dc2beb78905e25c0dc2bebc0737' + \
+            '0000000d4c2d64c447e701b74e3ad98c' + 'e65d13c3'
+        # Test good
+        self.assertEqual(cls.verify_mac(ntp.poly.polybytes(
+            ntp.util.hexstr2octets(good_pkt)), packet_end=48, mac_begin=48), True)
+        # Test bad
+        self.assertEqual(cls.verify_mac(ntp.poly.polybytes(
+            ntp.util.hexstr2octets(bad_pkt)), packet_end=48, mac_begin=48), False)
 
 
 if __name__ == "__main__":



View it on GitLab: https://gitlab.com/NTPsec/ntpsec/-/compare/191648fb7366252e38845166f37ed0b8d6aade96...cf99856a3babd8c4c4dcf340de201562ffbc64ce

-- 
View it on GitLab: https://gitlab.com/NTPsec/ntpsec/-/compare/191648fb7366252e38845166f37ed0b8d6aade96...cf99856a3babd8c4c4dcf340de201562ffbc64ce
You're receiving this email because of your account on gitlab.com.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.ntpsec.org/pipermail/vc/attachments/20200516/ed7bd727/attachment-0001.htm>


More information about the vc mailing list