SHA256 hashes support for SSH keys.
authorRadek Czajka <rczajka@rczajka.pl>
Mon, 19 Oct 2020 15:45:49 +0000 (17:45 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Mon, 19 Oct 2020 15:45:49 +0000 (17:45 +0200)
src/ssh_keys/admin.py
src/ssh_keys/migrations/0006_sshkey_sha256_hash.py [new file with mode: 0644]
src/ssh_keys/models.py
src/ssh_keys/utils.py
src/ssh_keys/views.py

index 6e75313..af736bb 100644 (file)
@@ -4,9 +4,9 @@ from .models import SSHKey
 
 
 class SSHKeyAdmin(admin.ModelAdmin):
-    fields = ['user',  'key', 'algorithm', 'bit_length', 'md5_hash', 'created_at', 'last_seen_at']
-    readonly_fields = ['algorithm', 'bit_length', 'md5_hash', 'created_at', 'last_seen_at']
-    list_display = ['comment', 'last_seen_at', 'user', 'md5_hash', 'algorithm', 'bit_length', 'created_at']
+    fields = ['user',  'key', 'algorithm', 'bit_length', 'sha256_hash', 'md5_hash', 'created_at', 'last_seen_at']
+    readonly_fields = ['algorithm', 'bit_length', 'sha256_hash', 'md5_hash', 'created_at', 'last_seen_at']
+    list_display = ['comment', 'last_seen_at', 'user', 'sha256_hash', 'algorithm', 'bit_length', 'created_at']
     ordering = (F('last_seen_at').desc(nulls_last=True),)
 
 admin.site.register(SSHKey, SSHKeyAdmin)
diff --git a/src/ssh_keys/migrations/0006_sshkey_sha256_hash.py b/src/ssh_keys/migrations/0006_sshkey_sha256_hash.py
new file mode 100644 (file)
index 0000000..316ff6b
--- /dev/null
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.8 on 2020-10-19 15:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ssh_keys', '0005_auto_20190401_0938'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='sshkey',
+            name='sha256_hash',
+            field=models.CharField(default='', editable=False, max_length=128, verbose_name='SHA256 hash'),
+            preserve_default=False,
+        ),
+    ]
index e4841ca..3bd49f8 100644 (file)
@@ -10,6 +10,7 @@ class SSHKey(models.Model):
     comment = models.CharField(_('comment'), max_length=255, editable=False)
     algorithm = models.CharField(_('algorithm'), max_length=32, editable=False)
     bit_length = models.IntegerField(_('bit length'), null=True, editable=False)
+    sha256_hash = models.CharField(_('SHA256 hash'), max_length=128, editable=False)
     md5_hash = models.CharField(_('MD5 hash'), max_length=128, editable=False)
     created_at = models.DateTimeField(_('created at'), auto_now_add=True)
     last_seen_at = models.DateTimeField(_('last seen at'), null=True, editable=False)
@@ -29,4 +30,5 @@ class SSHKey(models.Model):
         self.algorithm = det['algo']
         self.bit_length = det['bits']
         self.md5_hash = det['md5']
+        self.sha256_hash = det['sha256']
         return super().save(*args, **kwargs)
index e8756a2..c48d83c 100644 (file)
@@ -6,10 +6,10 @@ import subprocess
 def get_key_details(key):
     """
     >>> get_key_details('ssh-dss AAAAB3NzaC1kc3MAAACBAJxrocPXtCxwgg5yvOc1NLFFz/Fql4+7sOgMOkwWO6pxpJ4bPZgzZ0B17/HGKxQaot3Nc7vzdkC3MBrDDbKrX4n9qB9yJBd0Kkr5X0K7SnBKU+7fbg+rloUdYE78LS6ap05+xlJ8dU918DnS3KqcT/YQQXaTLrt/2DUOM1qxCI1XAAAAFQCJXLN0vYx7SIYMQ0zhv9IUT5WhgQAAAIAT2new16avxvs56zU87t1QQe0qwbQEIUygWW6vqnc9Lo9aSf21sM5WAHTkEnTVyiFSI6K6Q6bD2OUMvS2oWaoariW8EFKzg7/pufmThG0oAxkloc3j8gMO2+xuw7yHzP2pd6xgosNkqivpsGT1PKo+vM6x8p9B6PvipHPqhgFHWQAAAIEAhgFE3+gfPpfDIhaPP5Adx4Hm0VO3xBgOtafvunv3kP54kvHuTaD2uLwgcdOsMedv1/tqhJddh4+9ibwhlKbxKLHrQIcGSHCIY/BoA4RnpSBlGoXEc2buLoZ9IwANCIa2mp19Q/v4wwLnTJHabdMkNCiUn8NPEiHUPjgIj1uoCgo= epsilon')
-    {'algo': 'DSA', 'bits': 1024, 'md5': 'f6:78:a7:9e:6b:41:dc:31:f4:39:12:5f:2b:0c:7a:11', 'comment': 'epsilon'}
+    {'algo': 'DSA', 'bits': 1024, 'sha256': 'o2DscHLwvUsYJ7a5eYgc2B4hQO4rf19u17W9Hh/H2h8', 'comment': 'epsilon', 'md5': 'f6:78:a7:9e:6b:41:dc:31:f4:39:12:5f:2b:0c:7a:11'}
     """
     process = subprocess.run(
-        ['ssh-keygen', '-lEmd5', '-f-'],
+        ['ssh-keygen', '-lEsha256', '-f-'],
         input=key.encode('utf-8'),
         stdout=subprocess.PIPE,
         stderr=subprocess.PIPE)
@@ -18,16 +18,33 @@ def get_key_details(key):
 
     output = process.stdout.decode('utf-8').rstrip()
     m = re.match(
-        r'^(?P<bits>\d+) MD5:(?P<md5>[a-f0-9:]+) (?P<comment>.*) \((?P<algo>.*)\)$',
+        r'^(?P<bits>\d+) SHA256:(?P<sha256>[^ ]+) (?P<comment>.*) \((?P<algo>.*)\)$',
         output
     )
-    return {
+    data = {
         'algo':  m.group('algo'),
         'bits': int(m.group('bits')),
-        'md5': m.group('md5'),
+        'sha256': m.group('sha256'),
         'comment': m.group('comment'),
     }
 
+    process = subprocess.run(
+        ['ssh-keygen', '-lEmd5', '-f-'],
+        input=key.encode('utf-8'),
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE)
+    if process.returncode != 0:
+        raise ValueError(process.stderr.decode('utf-8'))
+
+    output = process.stdout.decode('utf-8').rstrip()
+    m = re.match(
+        r'^(?P<bits>\d+) MD5:(?P<md5>[a-f0-9:]+) (?P<comment>.*) \((?P<algo>.*)\)$',
+        output
+    )
+    data['md5'] = m.group('md5')
+
+    return data
+
 
 def parse_log_line(line, year=None, allow_future_days=7):
     """
@@ -35,8 +52,13 @@ def parse_log_line(line, year=None, allow_future_days=7):
     ...     'Jan  1 2:34:56 heta sshd[4112]: Accepted publickey for localuser from 0.0.0.0 port 33980 ssh2: RSA f6:78:a7:9e:6b:41:dc:31:f4:39:12:5f:2b:0c:7a:11',
     ...     year=2019).items())
     [('algo', 'RSA'), ('datetime', datetime.datetime(2019, 1, 1, 2, 34, 56)), ('host', 'heta'), ('ip', '0.0.0.0'), ('md5', 'f6:78:a7:9e:6b:41:dc:31:f4:39:12:5f:2b:0c:7a:11'), ('user', 'localuser')]
+    >>> sorted(parse_log_line(
+    ...     'Jan  1 2:34:56 heta sshd[4112]: Accepted publickey for localuser from 0.0.0.0 port 33980 ssh2: RSA SHA256:9FLLdMdArtybwaoS45qtNZSmcBsr1pR2t8c9O+XODBw',
+    ...     year=2019).items())
+    [('algo', 'RSA'), ('datetime', datetime.datetime(2019, 1, 1, 2, 34, 56)), ('host', 'heta'), ('ip', '0.0.0.0'), ('sha256', '9FLLdMdArtybwaoS45qtNZSmcBsr1pR2t8c9O+XODBw'), ('user', 'localuser')]
+
     """
-    logline_re = r'^(?P<time>\w{3}\s+\d+\s+\d?\d:\d\d:\d\d) (?P<host>\S+) .*: Accepted publickey for (?P<user>\S*) from (?P<ip>[\S]+) port .* ssh2: (?P<algo>\w+) (?P<md5>[a-f0-9:]+)$'
+    logline_re = r'^(?P<time>\w{3}\s+\d+\s+\d?\d:\d\d:\d\d) (?P<host>\S+) .*: Accepted publickey for (?P<user>\S*) from (?P<ip>[\S]+) port .* ssh2: (?P<algo>\w+) ((?P<md5>[a-f0-9:]+)|SHA256:(?P<sha256>[a-zA-Z0-9+/]+))$'
 
     m = re.match(logline_re, line)
     if m is None:
@@ -50,11 +72,16 @@ def parse_log_line(line, year=None, allow_future_days=7):
         if (dt - now).days > allow_future_days:
             dt = dt.replace(year=dt.year - 1)
 
-    return {
+    data = {
         'datetime': dt,
         'host': m['host'],
         'user': m['user'],
         'ip': m['ip'],
         'algo': m['algo'],
-        'md5': m['md5'],
     }
+    if m['md5']:
+        data['md5'] = m['md5']
+    if m['sha256']:
+        data['sha256'] = m['sha256']
+
+    return data
index 67401e3..73ba3e9 100644 (file)
@@ -64,15 +64,22 @@ def ssh_keys_seen(request):
             logger.error('Unparsed: ' + line)
             break
         dt = data['datetime']
-        key = data['algo'], data['md5']
+        algo = data['algo']
+        if 'md5' in data:
+            hash_type = 'md5'
+            hash_value = data['md5']
+        else:
+            hash_type = 'sha256'
+            hash_value = data['sha256']
+        key = algo, hash_type, hash_value
         last_seen[key] = max(last_seen.get(key, dt), dt)
 
     for key, dt in last_seen.items():
-        algo, md5 = key
+        algo, hash_type, hash_value = key
         SSHKey.objects.filter(
             Q(last_seen_at=None) | Q(last_seen_at__lt=dt),
             algorithm=algo,
-            md5_hash=md5
+            **{f'{hash_type}_hash': hash_value}
         ).update(
             last_seen_at=dt
         )