First version of a check for ssh keys
authorJoerg Jaspert <joerg@debian.org>
Mon, 12 May 2008 22:12:56 +0000 (00:12 +0200)
committerJoerg Jaspert <joerg@debian.org>
Mon, 12 May 2008 22:12:56 +0000 (00:12 +0200)
1  2 
ud-mailgate
userdir-ldap.conf

diff --combined ud-mailgate
@@@ -1,13 -1,9 +1,14 @@@
  #!/usr/bin/env python
  # -*- mode: python -*-
 -import userdir_gpg, userdir_ldap, sys, traceback, time, ldap, os;
 -import pwd
 -from userdir_gpg import *;
 -from userdir_ldap import *;
 +
 +#   Prior copyright probably rmurray, troup, joey, jgg -- weasel 2008
 +#   Copyright (c) 2008 Peter Palfrader <peter@palfrader.org>
++#   Copyright (c) 2008 Joerg Jaspert <joerg@debian.org>
 +
- import userdir_gpg, userdir_ldap, sys, traceback, time, ldap, os;
- import pwd
- from userdir_gpg import *;
- from userdir_ldap import *;
++import userdir_gpg, userdir_ldap, sys, traceback, time, ldap, os, commands
++import pwd, tmpfile
++from userdir_gpg import *
++from userdir_ldap import *
  
  # Error codes from /usr/include/sysexits.h
  ReplyTo = ConfModule.replyto;
@@@ -15,6 -11,6 +16,7 @@@ PingFrom = ConfModule.pingfrom
  ChPassFrom = ConfModule.chpassfrom;
  ChangeFrom = ConfModule.changefrom;
  ReplayCacheFile = ConfModule.replaycachefile;
++SSHFingerprintFile = ConfModule.fingerprintfile
  
  EX_TEMPFAIL = 75;
  EX_PERMFAIL = 65;      # EX_DATAERR
@@@ -27,6 -23,6 +29,8 @@@ mailWhitelist = {
  SeenList = {}
  DNS = {}
  
++SSHFingerprint = re.compile('^(\d+) ([0-9a-f\:]{47}) (.+)$')
++
  ArbChanges = {"c": "..",
              "l": ".*",
              "facsimileTelephoneNumber": ".*",
@@@ -44,7 -40,6 +48,7 @@@
                "mailDisableMessage": ".*",
              "mailGreylisting": "^(TRUE|FALSE)$",
              "mailCallout": "^(TRUE|FALSE)$",
 +            "VoIP": ".*",
  };
  
  DelItems = {"c": None,
@@@ -72,7 -67,6 +76,7 @@@
              "mailRHSBL": None,
              "mailWhitelist": None,
              "mailDisableMessage": None,
 +            "VoIP": None,
              };
  
  # Decode a GPS location from some common forms
@@@ -218,16 -212,16 +222,46 @@@ def DoPosition(Str,Attrs)
     Attrs.append((ldap.MOD_REPLACE,"longitude",sLong));
     return "Position set to %s/%s (%s/%s decimal degrees)"%(sLat,sLong,Lat,Long);
  
++# Load bad ssh fingerprints
++def LoadBadSSH():
++   f = open(SSHFingerprintFile, "r")
++   bad = []
++   FingerprintLine = re.compile('^([0-9a-f\:]{47}).*$')
++   for line in f.readlines():
++      Match = FingerprintLine.match(line)
++      if Match is not None:
++         g = Match.groups()
++         bad.append(g[0])
++   return bad
++
  # Handle an SSH authentication key, the line format is:
  #  [options] 1024 35 13188913666680[..] [comment]
--def DoSSH(Str,Attrs):
++def DoSSH(Str,Attrs, badkeys):
     Match = SSH2AuthSplit.match(Str);
     if Match == None:
        Match = re.compile('^1024 (\d+) ').match(Str)
        if Match is not None:
           return "SSH1 keys not supported anymore"
        return None;
--   
++
++   (fd, path) = tempfile.mkstemp("", "sshkeytry")
++   f = open(path, "w")
++   f.write(Str)
++   f.close
++   (result, output) = commands.getstatusoutput("ssh-keygen -f %s -l" % (path))
++   os.remove(path)
++   if (result != 0):
++      sys.stderr.write("ssh-keygen -l invocation failed!\n%s\n" % (output))
++      sys.exit(result)
++
++   Match = SSHFingerprint.match(output)
++
++   g = Match.groups()
++   if (g[0] < 1024):
++      return "SSH keys must have at least 1024 bits, not added"
++   elif g[0] in badkeys:
++      return "Submitted SSH Key known to be bad and insecure, not added"
++
     global SeenKey;
     if SeenKey:
       Attrs.append((ldap.MOD_ADD,"sshRSAAuthKey",Str));
     return "SSH Keys replaced with "+FormatSSHAuth(Str);
  
  # Handle changing a dns entry
 -#  host in a 12.12.12.12
 -#  host in cname foo.bar.    <- Trailing dot is required
 +#  host IN A     12.12.12.12
 +#  host IN AAAA  1234::5678
 +#  host IN CNAME foo.bar.    <- Trailing dot is required
 +#  host IN MX    foo.bar.    <- Trailing dot is required
  def DoDNS(Str,Attrs,DnRecord):
 -   cname = re.match("^[-\w]+\s+in\s+cname\s+[-\w.]+\.$",Str,re.IGNORECASE);
 -   if re.match('^[-\w]+\s+in\s+a\s+\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$',\
 -        Str,re.IGNORECASE) == None and cname == None and \
 -      re.match("^[-\w]+\s+in\s+mx\s+\d{1,3}\s+[-\w.]+\.$",Str,re.IGNORECASE) == None:
 -     return None;     
 +   cnamerecord = re.match("^[-\w]+\s+IN\s+CNAME\s+([-\w.]+\.)$",Str,re.IGNORECASE)
 +   arecord     = re.match('^[-\w]+\s+IN\s+A\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$',Str,re.IGNORECASE)
 +   mxrecord    = re.match("^[-\w]+\s+IN\s+MX\s+(\d{1,3})\s+([-\w.]+\.)$",Str,re.IGNORECASE)
 +   #aaaarecord  = re.match('^[-\w]+\s+IN\s+AAAA\s+((?:[0-9a-f]{1,4})(?::[0-9a-f]{1,4})*(?::(?:(?::[0-9a-f]{1,4})*|:))?)$',Str,re.IGNORECASE)
 +   aaaarecord  = re.match('^[-\w]+\s+IN\s+AAAA\s+([A-F0-9:]{2,39})$',Str,re.IGNORECASE)
 +
 +   if cnamerecord == None and\
 +      arecord == None and\
 +      mxrecord == None and\
 +      aaaarecord == None:
 +     return None;
  
     # Check if the name is already taken
 -   G = re.match('^([-\w+]+)\s',Str).groups();
 +   G = re.match('^([-\w+]+)\s',Str)
 +   if G == None:
 +     raise Error, "Hostname not found although we already passed record syntax checks"
 +   hostname = G.group(1)
  
     # Check for collisions
     global l;
     #  since we accept either.  It'd probably be better to parse the
     #  incoming string in order to construct what we feed LDAP rather
     #  than just passing it through as is.]
 -   filter = "(|(dnsZoneEntry=%s       *)(dnsZoneEntry=%s *))" % (G[0], G[0])
 +   filter = "(|(dnsZoneEntry=%s       *)(dnsZoneEntry=%s *))" % (hostname, hostname)
     Rec = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,filter,["uid"]);
     for x in Rec:
        if GetAttr(x,"uid") != GetAttr(DnRecord,"uid"):
     global SeenDNS;
     global DNS;
  
 -   if cname:
 -     if DNS.has_key(G[0]):
 +   if cnamerecord:
 +     if DNS.has_key(hostname):
         return "CNAME and other RR types not allowed: "+Str
       else:
 -       DNS[G[0]] = 2
 +       DNS[hostname] = 2
     else:
 -     if DNS.has_key(G[0]) and DNS[G[0]] == 2:
 +     if DNS.has_key(hostname) and DNS[hostname] == 2:
         return "CNAME and other RR types not allowed: "+Str
       else:
 -       DNS[G[0]] = 1
 -     
 +       DNS[hostname] = 1
 +
 +   if cnamerecord != None:
 +     sanitized = "%s IN CNAME %s" % (hostname, cnamerecord.group(1))
 +   elif arecord != None:
 +     ipaddress = arecord.group(1)
 +     for quad in ipaddress.split('.'):
 +       if not (int(quad) >=0 and int(quad) <= 255):
 +         return "Invalid quad %s in IP address %s in line %s" %(quad, ipaddress, Str)
 +     sanitized = "%s IN A %s"% (hostname, ipaddress)
 +   elif mxrecord != None:
 +     priority = mxrecord.group(1)
 +     mx = mxrecord.group(2)
 +     sanitized = "%s IN MX %s %s" % (hostname, priority, mx)
 +   elif aaaarecord != None:
 +     ipv6address = aaaarecord.group(1)
 +     parts = ipv6address.split(':')
 +     if len(parts) > 8:
 +       return "Invalid IPv6 address (%s): too many parts"%(ipv6address)
 +     if len(parts) <= 2:
 +       return "Invalid IPv6 address (%s): too few parts"%(ipv6address)
 +     if parts[0] == "":
 +       parts.pop(0)
 +     if parts[-1] == "":
 +       parts.pop(-1)
 +     seenEmptypart = False
 +     for p in parts:
 +       if len(p) > 4:
 +         return "Invalid IPv6 address (%s): part %s is longer than 4 characters"%(ipv6address, p)
 +       if p == "":
 +         if seenEmptypart:
 +           return "Invalid IPv6 address (%s): more than one :: (nothing in between colons) is not allowed"%(ipv6address)
 +         seenEmptypart = True
 +     sanitized = "%s IN AAAA %s" % (hostname, ipv6address)
 +   else:
 +     raise Error, "None of the types I recognize was it.  I shouldn't be here.  confused."
 +
     if SeenDNS:
 -     Attrs.append((ldap.MOD_ADD,"dnsZoneEntry",Str));
 -     return "DNS Entry added "+Str;
 -      
 -   Attrs.append((ldap.MOD_REPLACE,"dnsZoneEntry",Str));
 +     Attrs.append((ldap.MOD_ADD,"dnsZoneEntry",sanitized));
 +     return "DNS Entry added "+sanitized;
 +
 +   Attrs.append((ldap.MOD_REPLACE,"dnsZoneEntry",sanitized));
     SeenDNS = 1;
 -   return "DNS Entry replaced with "+Str;
 +   return "DNS Entry replaced with "+sanitized;
  
  # Handle an RBL list (mailRBL, mailRHSBL, mailWhitelist)
  def DoRBL(Str,Attrs):
@@@ -371,11 -319,11 +405,12 @@@ def HandleChange(Reply,DnRecord,Key)
        try:
           if Line == "show":
             Show = 1;
--         Res = "OK";
++           Res = "OK";
           else:
--           Res = DoPosition(Line,Attrs) or DoDNS(Line,Attrs,DnRecord) or \
--                 DoArbChange(Line,Attrs) or DoSSH(Line,Attrs) or \
--               DoDel(Line,Attrs) or DoRBL(Line,Attrs);
++            badkeys = LoadBadSSH()
++            Res = DoPosition(Line,Attrs) or DoDNS(Line,Attrs,DnRecord) or \
++                  DoArbChange(Line,Attrs) or DoSSH(Line,Attrs,badkeys) or \
++                  DoDel(Line,Attrs) or DoRBL(Line,Attrs)
        except:
           Res = None;
           Result = Result + "==> %s: %s\n" %(sys.exc_type,sys.exc_value);
diff --combined userdir-ldap.conf
@@@ -24,6 -24,6 +24,7 @@@ changefrom = "change@" + maildomain
  templatesdir = "/etc/userdir-ldap/templates/";
  replaycachefile = "/var/cache/userdir-ldap/mail/replay";
  #replaycachefile = "/tmp/replay";
++fingerprintfile = "/etc/userdir-ldap/badfingerprints"
  
  # Echelon
  ech_errorlog = "/org/db.debian.org/mail/Log/ech-errors.log"