Add support for setting a TOTP seed
authorTollef Fog Heen <tfheen@err.no>
Tue, 8 Aug 2017 22:37:56 +0000 (00:37 +0200)
committerTollef Fog Heen <tfheen@err.no>
Tue, 8 Aug 2017 22:38:36 +0000 (00:38 +0200)
This still needs a bit of docs, but is functionally working.

debian/changelog
templates/totp-seed-changed [new file with mode: 0644]
ud-generate
ud-mailgate
userdir-ldap.schema

index 35dd065..4ee8ee2 100644 (file)
@@ -10,6 +10,10 @@ userdir-ldap (0.3.90) UNRELEASED; urgency=medium
     them per-host where needed so we can accomodate per-host ssh
     authorized-keys.
 
     them per-host where needed so we can accomodate per-host ssh
     authorized-keys.
 
+  [ Tollef Fog Heen ]
+  * Add totpSeed to LDAP schema.
+  * Add support for changing TOTP seed by mailing ud-mailgate.
+
  -- Paul Wise <pabs@debian.org>  Sat, 17 Jun 2017 14:38:00 +0800
 
 userdir-ldap (0.3.89) unstable; urgency=medium
  -- Paul Wise <pabs@debian.org>  Sat, 17 Jun 2017 14:38:00 +0800
 
 userdir-ldap (0.3.89) unstable; urgency=medium
diff --git a/templates/totp-seed-changed b/templates/totp-seed-changed
new file mode 100644 (file)
index 0000000..52662f6
--- /dev/null
@@ -0,0 +1,14 @@
+From: __FROM__
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 8bit
+Subject: TOTP seed changed
+
+Hello __EMAIL__!
+
+Your TOTP seed has been updated. Enclosed below is the new seed
+encrypted with your key.
+
+Please email __ADMIN__ if you have any questions.
+
+__PASSWORD__
index a4a74b5..9dcf0a3 100755 (executable)
@@ -449,6 +449,28 @@ def GenRtcPassword(accounts, File):
       Die(File, None, F)
       raise
 
       Die(File, None, F)
       raise
 
+# Generate the TOTP auth file
+def GenTOTPSeed(accounts, File):
+   F = None
+   try:
+      OldMask = os.umask(0077)
+      F = open(File, "w", 0600)
+      os.umask(OldMask)
+
+      F.write("# Option User Prefix Seed\n")
+      for a in accounts:
+         if a.is_guest_account(): continue
+         if not 'totpSeed' in a: continue
+         if not a.pw_active(): continue
+
+         Line = "HOTP/T30/6 %s - %s" % (a['uid'], a['totpSeed'])
+         Line = Sanitize(Line) + "\n"
+         F.write("%s" % (Line))
+   except:
+      Die(File, None, F)
+      raise
+
+
 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
    OldMask = os.umask(0077)
    tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
    OldMask = os.umask(0077)
    tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
@@ -1126,7 +1148,7 @@ def get_accounts(ldap_conn):
                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
                     "mailContentInspectionAction", "webPassword", "rtcPassword",\
                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
                     "mailContentInspectionAction", "webPassword", "rtcPassword",\
-                    "bATVToken"])
+                    "bATVToken", "totpSeed"])
 
    if passwd_attrs is None:
       raise UDEmptyList, "No Users"
 
    if passwd_attrs is None:
       raise UDEmptyList, "No Users"
@@ -1214,6 +1236,7 @@ def generate_all(global_dir, ldap_conn):
    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
    GenWebPassword(accounts, global_dir + "web-passwords")
    GenRtcPassword(accounts, global_dir + "rtc-passwords")
    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
    GenWebPassword(accounts, global_dir + "web-passwords")
    GenRtcPassword(accounts, global_dir + "rtc-passwords")
+   GenTOTPSeed(accounts, global_dir + "users.oath")
    GenKeyrings(global_dir)
 
    # Compatibility.
    GenKeyrings(global_dir)
 
    # Compatibility.
@@ -1344,6 +1367,9 @@ def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
    if 'RTC-PASSWORDS' in ExtraList:
       DoLink(global_dir, OutDir, "rtc-passwords")
 
    if 'RTC-PASSWORDS' in ExtraList:
       DoLink(global_dir, OutDir, "rtc-passwords")
 
+   if 'TOTP' in ExtraList:
+      DoLink(global_dir, OutDir, "users.oath")
+
    if 'KEYRING' in ExtraList:
       for k in Keyrings:
          bn = os.path.basename(k)
    if 'KEYRING' in ExtraList:
       for k in Keyrings:
          bn = os.path.basename(k)
index c54aee5..427a024 100755 (executable)
@@ -11,6 +11,7 @@ import userdir_gpg, userdir_ldap, sys, traceback, time, ldap, os, commands
 import pwd, tempfile
 import subprocess
 import email, email.parser
 import pwd, tempfile
 import subprocess
 import email, email.parser
+import binascii
 
 from userdir_gpg import *
 from userdir_ldap import *
 
 from userdir_gpg import *
 from userdir_ldap import *
@@ -687,6 +688,28 @@ def HandleChPass(Reply,DnRecord,Key):
 
    return Reply;
 
 
    return Reply;
 
+def HandleChTOTPSeed(Reply, DnRecord, Key):
+   # Generate a random seed
+   seed = binascii.hexlify(open("/dev/urandom", "r").read(32))
+   msg = GPGEncrypt("Your new TOTP seed is '%s'\n" % (seed,), "0x"+Key[1],Key[4]);
+
+   if msg is None:
+      raise UDFormatError, "Unable to generate the encrypted reply, gpg failed.";
+
+   Subst = {};
+   Subst["__FROM__"] = ChPassFrom
+   Subst["__EMAIL__"] = EmailAddress(DnRecord)
+   Subst["__PASSWORD__"] = msg
+   Subst["__ADMIN__"] = ReplyTo
+   Reply = Reply + TemplateSubst(Subst, open(TemplatesDir+"totp-seed-changed", "r").read())
+
+   l = connect_to_ldap_and_check_if_locked(DnRecord)
+   # Modify the password
+   Rec = [(ldap.MOD_REPLACE, "totpSeed", seed)]
+   Dn = "uid=" + GetAttr(DnRecord,"uid") + "," + BaseDn
+   l.modify_s(Dn,Rec)
+   return Reply;
+
 def HandleChKrbPass(Reply,DnRecord,Key):
    # Connect to the ldap server, will throw an exception if account locked.
    l = connect_to_ldap_and_check_if_locked(DnRecord)
 def HandleChKrbPass(Reply,DnRecord,Key):
    # Connect to the ldap server, will throw an exception if account locked.
    l = connect_to_ldap_and_check_if_locked(DnRecord)
@@ -814,6 +837,8 @@ try:
          Reply = HandleChPass(Reply,Attrs[0],pgp.key_info);
       elif PlainText.strip().find("Please change my Kerberos password") >= 0:
          Reply = HandleChKrbPass(Reply,Attrs[0],pgp.key_info);
          Reply = HandleChPass(Reply,Attrs[0],pgp.key_info);
       elif PlainText.strip().find("Please change my Kerberos password") >= 0:
          Reply = HandleChKrbPass(Reply,Attrs[0],pgp.key_info);
+      elif PlainText.strip().find("Please change my TOTP seed") >= 0:
+         Reply = HandleChTOTPSeed(Reply, Attrs[0], pgp.key_info)
       else:
          raise UDFormatError,"Please send a signed message where the first line of text is the string 'Please change my Debian password' or some other string we accept here.";
    elif sys.argv[1] == "change":
       else:
          raise UDFormatError,"Please send a signed message where the first line of text is the string 'Please change my Debian password' or some other string we accept here.";
    elif sys.argv[1] == "change":
index 2774250..d0def19 100644 (file)
 #   .43 - webPassword
 #   .44 - rtcPassword
 #   .45 - rebootPolicy
 #   .43 - webPassword
 #   .44 - rtcPassword
 #   .45 - rebootPolicy
+#   .46 - totpSeed
 #
 # .3 - experimental LDAP objectClasses
 #   .1 - debianDeveloper
 #
 # .3 - experimental LDAP objectClasses
 #   .1 - debianDeveloper
@@ -544,6 +545,12 @@ attributetype ( 1.3.6.1.4.1.9586.100.4.4.45
        SUBSTR caseIgnoreIA5SubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
 
        SUBSTR caseIgnoreIA5SubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
 
+attributetype ( 1.3.6.1.4.1.9586.100.4.4.46
+       NAME 'totpSeed'
+       DESC 'Seed for TOTP authentication'
+       EQUALITY octetStringMatch
+       SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
+
 # Public object classes
 
 objectclass ( 1.3.6.1.4.1.9586.100.4.1.1
 # Public object classes
 
 objectclass ( 1.3.6.1.4.1.9586.100.4.1.1
@@ -551,7 +558,7 @@ objectclass ( 1.3.6.1.4.1.9586.100.4.1.1
        DESC 'Abstraction of an account with POSIX attributes and UTF8 support'
        SUP top AUXILIARY
        MUST ( cn $ uid $ uidNumber $ gidNumber )
        DESC 'Abstraction of an account with POSIX attributes and UTF8 support'
        SUP top AUXILIARY
        MUST ( cn $ uid $ uidNumber $ gidNumber )
-       MAY ( userPassword $ loginShell $ gecos $ homeDirectory $ description $ mailDisableMessage $ sudoPassword $ webPassword $ rtcPassword ) )
+       MAY ( userPassword $ loginShell $ gecos $ homeDirectory $ description $ mailDisableMessage $ sudoPassword $ webPassword $ rtcPassword $ totpSeed ) )
 
 objectclass ( 1.3.6.1.4.1.9586.100.4.1.2
        NAME 'debianGroup'
 
 objectclass ( 1.3.6.1.4.1.9586.100.4.1.2
        NAME 'debianGroup'