Submit Hint Search The Forums LinksStatsPollsHeadlinesRSS
14,000 hints and counting!

Create protected passwordless ssh logins - Part 3 of 3 UNIX
Part 3: Implementing your own sshLogin

[NOTE: - this is part 3 of a 3-part tip. Please see part 1 for background info.]

This is the trickiest and most complicated part. It assumes you have completed parts (1) and (2) successfully. Read the rest of the hint for the detailed how-to...

  1. Let's create a directory where we'll put all the scripts we're about to create. I chose ~/Library/Libexec
    % mkdir ~/Library/Libexec
    
  2. We're going to need some simple utilities for reading and writing to ~/.MacOSX/environment.plist. In case you're not familiar with this file, it is read as you login and used to set environment variables that are available to any application you run while you are logged in. It's an XML formatted file, and is well-formatted, so I was able to create the utilities I needed in awk:

    1. Create ~/Library/Libexec/getenvplist with the following contents:
      #!/bin/sh
      set -e
      PATH=/usr/bin:/bin
      export PATH
      plist=$HOME/.MacOSX/environment.plist
      awk '
        /.*/ {
          s = index($0, "" ) + 5
          e = index($0, "")
          key = substr($0, s, e-s)
          next
        }
        /.*/ {
          s = index($0, "" ) + 8
          e = index($0, "")
          string = substr($0, s, e-s)
          printf "%s="%s"; export %sn", key, string, key
          next
        }
      ' $plist
      
    2. Create ~/Library/Libexec/setenvplist with the following contents:
      #!/bin/sh
      set -e
      PATH=/usr/bin:/bin
      export PATH
      plist=$HOME/.MacOSX/environment.plist
      plist_bak="${plist}.bak"
      rm -f $plist_bak
      cp -f "$plist" "$plist_bak"
      awk '
        (in_dict == 0) { print }
        // { 
          in_dict = 1 
          next
        }
        /.*/ {
          s = index($0, "" ) + 5
          e = index($0, "")
          key = substr($0, s, e-s)
          next
        }
        /.*/ {
          s = index($0, "" ) + 8
          e = index($0, "")
          string = substr($0, s, e-s)
          keys[key] = string
          next
        }
        // {
          in_dict = 0
          for (i = 2; i < ARGC; i++) {
            split(ARGV[i], a, "=")
            keys[a[1]] = a[2]
          }
          for (key in keys) {
            val = keys[key]
            if (length(val) > 0) {
              printf "t%sn", key
              printf "t%sn", val
            }
          }
          print
          next
        }
      ' $plist_bak "$@" > $plist
      plutil -convert xml1 $plist
      
    [NOTE - I can't promise these utilities will work for all valid environment.plist files (the parsing is naive), but they work fine for my purposes.]

  3. We're also going to need to "wrap" the default ssh-add, ssh-agent, and ssh-askpass commands:

    1. Create ~/Library/Libexec/ssh-add with the following contents:
      #!/bin/sh
      DISPLAY=bogus
      SSH_ASKPASS=$HOME/Library/Libexec/ssh-askpass
      export DISPLAY SSH_ASKPASS
      /usr/bin/ssh-add < /dev/null
      
    2. Create ~/Library/Libexec/ssh-agent with the following contents:
      #!/bin/sh
      eval `/usr/bin/ssh-agent -s`
      $HOME/Library/Libexec/setenvplist 
              SSH_AUTH_SOCK=$SSH_AUTH_SOCK 
              SSH_AGENT_PID=$SSH_AGENT_PID 
              SSH_ASKPASS=$HOME/Library/Libexec/ssh-askpass 
              CVS_RSH=ssh
      
    3. Create ~/Library/Libexec/ssh-askpass with the following contents:
      #!/bin/sh
      exec $HOME/Library/Libexec/keychain someuser@somewhere.org sshLogin-Key
      
      Set someuser@somewhere.org to your username and machine name for now. We'll setup the key in keychain access later.

    4. While we're at it, copy the keychain utility previously described into your Libexec directory.

    5. Make sure all the scripts you've just created are executable:
      % chmod 755 ~/Library/Libexec/*
      
  4. Next, let's create our per-user Hooks directory and Login/Logout Hooks:

    1. Create your per-user Hooks directory:
      % mkdir ~/Library/Hooks
      

    2. Create ~/Library/Hooks/LoginHook with the following contents:
      #!/bin/sh
      
      PATH=/usr/bin:/bin
      export PATH
      
      hook=`basename $0`
      exec > "$HOME/Library/Logs/${hook}.log" 2>&1
      
      LoginHook () {
        . $HOME/Library/Libexec/ssh-agent
        # give keychain time to unlock before calling ssh-add
        (sleep 2; $HOME/Library/Libexec/ssh-add) &
      }
      
      LogoutHook () {
        eval `$HOME/Library/Libexec/getenvplist`
        /usr/bin/ssh-agent -k
      }
      
      $hook
      
    3. Create a link to this file, called LogoutHook:
      % ln ~/Library/Hooks/LoginHook ~/Library/Hooks/LogoutHook
      
    4. Make sure these are executable:
      chmod 755 ~/Library/Hooks/LoginHook
      

  5. Almost done, fire-up Keychain Access. Select "New Password Item..." from the File menu. For Name, enter whatever you used for "someuser@somewhere.org" above. For Account: enter "sshLogin-Key". For Password, enter the passphase that protects your ssh keys.

  6. Quit Keychain Access and let's verify that the keychain program has access to the passphase:
    % ~/Library/Libexec/keychain someuser@somewhere.org sshLogin-Key
    

    You should get a pop-up window prompting you to grant permission to keychain to access the key. Click "Always Allow". You should see your passphrase emitted on the command line. Run keychain again and this time it should emit the passphrase without the pop-up window. Good.

Okay, that's it. You should be able to logout and log back in and you'll be able to magically ssh to places (that you've setup authorized_keys for...) w/o having to type a password. And, when you're logged out, everything is secure.

So what's going on here. Let's follow the chain of events.

  1. Upon login, the global com.apple.loginwindow plist is read and /Library/Hooks/LoginHook is executed as root, with the username that just logged in as its argument.
  2. /Library/Hooks/LoginHook then executes /Users//Hooks/LoginHook (if it exists) as the user.
  3. The per-user LoginHook then runs ~/Library/Libexec/ssh-agent. In the background (so as not to stall the login...) it then waits 2 seconds and calls ~/Library/Libexec/ssh-add.
  4. ~/Library/Libexec/ssh-agent calls the system ssh-agent. This starts up ssh-agent. Upon startup, ssh-agent emits some important environment variables. Namely, SSH_AUTH_SOCK and SSH_AGENT_PID. These are "captured" by ~/Library/Libexec/ssh-agent since it evals the output of "/usr/bin/ssh-agent -s". ~/Library/Libexec/ssh-agent then calls setenvplist to add these variables to ~/.MacOSX/environment.plist. It also adds a couple other ssh related environment variables.
  5. The per-user LoginHook then finishes which allows the login process to continue. loginwindow will read the updated .MacOSX/environment.plist and these environment variables will then be available to any user-run processes.
  6. 2 seconds or so after the per-user LoginHook was run, it runs ~/Library/Libexec/ssh-add. The delay was to give the system time to unlock the default keychain. ~/Library/Libexec/ssh-add sets up DISPLAY and SSH_ASKPASS appropriately then calls the system ssh-add. ssh-add, seeing both DISPLAY and SSH_ASKPASS set, and with stdin redirected from /dev/null, will then execute the SSH_ASKPASS program, which in this case is ~/Library/Libexec/ssh-askpass.
  7. ~/Library/Libexec/ssh-askpass, finally, just executes our keychain utility with the appropriate arguments. That utility will then emit the passphrase on stdout, which is "consumed" by /usr/bin/ssh-add. /usr/bin/ssh-add then unlocks your ssh keys and provides them to ssh-agent.

Now for what happens on logout:

  1. Upon logout, the global com.apple.loginwindow plist is read and /Library/Hooks/LogoutHook is executed as root, with the username that just logged out as its argument.
  2. /Library/Hooks/LogoutHook then executes /Users//Hooks/LogoutHook (if it exists) as the user.
  3. The per-user LogoutHook then run getenvplist. This is needed because the user has already been logged out, and the per-user environment variables are no longer available. getenvplist retrieves the variables from .MacOSX/environment.plist and makes them available (which is why the output is "eval'd"). ssh-agent-k is then run which kills the running per-user ssh-agent process.

Simple, eh? :-)

    •    
  • Currently 2.50 / 5
  • 1
  • 2
  • 3
  • 4
  • 5
  (2 votes cast)
 
[18,306 views]  

Create protected passwordless ssh logins - Part 3 of 3 | 9 comments | Create New Account
Click here to return to the 'Create protected passwordless ssh logins - Part 3 of 3' hint
The following comments are owned by whoever posted them. This site is not responsible for what they say.
Create protected passwordless ssh logins - Part 3 of 3
Authored by: jaysoffian on Jan 12, '04 12:32:01PM
Argh, my less than and greater than signs got swallowed by the submission system. Here's the awk scripts again:

  1. Create ~/Library/Libexec/getenvplist with the following contents:
    #!/bin/sh
    set -e
    PATH=/usr/bin:/bin
    export PATH
    plist=$HOME/.MacOSX/environment.plist
    awk '
            /<key>.*<\/key>/ {
                    s = index($0, "<key>" ) + 5
                    e = index($0, "</key>")
                    key = substr($0, s, e-s)
                    next
            }
            /<string>.*<\/string>/ {
                    s = index($0, "<string>" ) + 8
                    e = index($0, "</string>")
                    string = substr($0, s, e-s)
                    printf "%s=\"%s\"; export %s\n", key, string, key
                    next
            }
    ' $plist
    

  2. Create ~/Library/Libexec/setenvplist with the following contents:
    #!/bin/sh
    set -e
    PATH=/usr/bin:/bin
    export PATH
    plist=$HOME/.MacOSX/environment.plist
    plist_bak="${plist}.bak"
    rm -f $plist_bak
    cp -f "$plist" "$plist_bak"
    awk '
            (in_dict == 0) { print }
            /<dict>/ { 
                    in_dict = 1 
                    next
            }
            /<key>.*<\/key>/ {
                    s = index($0, "<key>" ) + 5
                    e = index($0, "</key>")
                    key = substr($0, s, e-s)
                    next
            }
            /<string>.*<\/string>/ {
                    s = index($0, "<string>" ) + 8
                    e = index($0, "</string>")
                    string = substr($0, s, e-s)
                    keys[key] = string
                    next
            }
            /<\/dict>/ {
                    in_dict = 0
                    for (i = 2; i < ARGC; i++) {
                            split(ARGV[i], a, "=")
                            keys[a[1]] = a[2]
                    }
                    for (key in keys) {
                            val = keys[key]
                            if (length(val) > 0) {
                                    printf "\t<key>%s</key>\n", key
                                    printf "\t<string>%s</string>\n", val
                            }
                    }
                    print
                    next
            }
    ' $plist_bak "$@" > $plist
    plutil -convert xml1 $plist
    

My summary also had some less than and greater than signs swallowed. Here again:

So what's going on here. Let's follow the chain of events.

  1. Upon login, the global com.apple.loginwindow plist is read and /Library/Hooks/LoginHook is executed as root, with the username that just logged in as its argument.

  2. /Library/Hooks/LoginHook then executes /Users/<username>/Hooks/LoginHook (if it exists) as the user.

  3. The per-user LoginHook then runs ~/Library/Libexec/ssh-agent. In the background (so as not to stall the login...) it then waits 2 seconds and calls ~/Library/Libexec/ssh-add.

  4. ~/Library/Libexec/ssh-agent calls the system ssh-agent. This starts up ssh-agent. Upon startup, ssh-agent emits some important environment variables. Namely, SSH_AUTH_SOCK and SSH_AGENT_PID. These are "captured" by ~/Library/Libexec/ssh-agent since it evals the output of "/usr/bin/ssh-agent -s". ~/Library/Libexec/ssh-agent then calls setenvplist to add these variables to ~/.MacOSX/environment.plist. It also adds a couple other ssh related environment variables.

  5. The per-user LoginHook then finishes which allows the login process to continue. loginwindow will read the updated .MacOSX/environment.plist and these environment variables will then be available to any user-run processes.

  6. 2 seconds or so after the per-user LoginHook was run, it runs ~/Library/Libexec/ssh-add. The delay was to give the system time to unlock the default keychain. ~/Library/Libexec/ssh-add sets up DISPLAY and SSH_ASKPASS appropriately then calls the system ssh-add. ssh-add, seeing both DISPLAY and SSH_ASKPASS set, and with stdin redirected from /dev/null, will then execute the SSH_ASKPASS program, which in this case is ~/Library/Libexec/ssh-askpass.

  7. ~/Library/Libexec/ssh-askpass, finally, just executes our keychain utility with the appropriate arguments. That utility will then emit the passphrase on stdout, which is read by /usr/bin/ssh-add (on stdin). /usr/bin/ssh-add then unlocks your ssh keys and provides them to ssh-agent.

Now for what happens on logout:

  1. Upon logout, the global com.apple.loginwindow plist is read and /Library/Hooks/LogoutHook is executed as root, with the username that just logged out as its argument.

  2. /Library/Hooks/LogoutHook then executes /Users/<username>/Hooks/LogoutHook (if it exists) as the user.

  3. The per-user LogoutHook then run getenvplist. This is needed because the user has already been logged out, and the per-user environment variables are no longer available. getenvplist retrieves the variables from .MacOSX/environment.plist and makes them available (which is why the output is "eval'd"). ssh-agent-k is then run which kills the running per-user ssh-agent process.


[ Reply to This | # ]
Create protected passwordless ssh logins - Part 3 of 3
Authored by: KOHb on Jan 12, '04 12:51:00PM
I like the SSHKeychain app. It has support for forgetting your passphrase when your machine wakes up, and it can juggle multiple keys.

[ Reply to This | # ]
Why
Authored by: leif on Jan 13, '04 08:21:35AM
How did you get from passwordless-ssh to parsing xml with awk ?!!!

I use "SSH Agent" http://www.phil.uu.nl/~xges/ssh/ and it works great.

[ Reply to This | # ]
Create protected passwordless ssh logins - Part 3 of 3
Authored by: yklts1 on Jan 13, '04 11:56:12PM

Hello,
Will these scripts work on Jaguar?
Thanks.



[ Reply to This | # ]
Create protected passwordless ssh logins - Part 3 of 3
Authored by: bluehz on Jan 14, '04 09:48:37AM

I'm sure the author has his reasons for doing it this way. I personally have used "sshLogin" for over year and it works perfectly for me.

http://www.synthemesc.com/sshLogin/sshLogin.html



[ Reply to This | # ]
Create protected passwordless ssh logins - Part 3 of 3
Authored by: jaysoffian on Mar 28, '06 07:37:48AM
Here's my current User LoginHook/LogoutHook. See part 1 for the current system LoginHook/LogoutHook which enables these. Save this snippet as UserHook, follow the instructions in the comments to install, then you can remove UserHook:

#!/bin/sh
#
# INSTALLATION INSTRUCTIONS:
# mkdir $HOME/Library/Hooks
# cp UserHook $HOME/Library/Hooks/LoginHook
# cp UserHook $HOME/Library/Hooks/LogoutHook
# chmod -R 755 $HOME/Library/Hooks
#
PATH=/usr/bin:/bin
export PATH

hook=`basename $0`
exec > "$HOME/Library/Logs/${hook}.log" 2>&1

LoginHook () {
	. $HOME/Library/Libexec/ssh-agent
	# give keychain time to unlock before calling ssh-add
	(sleep 2; $HOME/Library/Libexec/ssh-add) &
}

LogoutHook () {
	eval `$HOME/Library/Libexec/envx`
	/usr/bin/ssh-agent -k
}

$hook


[ Reply to This | # ]
Create protected passwordless ssh logins - Part 3 of 3
Authored by: jaysoffian on Mar 28, '06 07:44:28AM
Here's the current versions of the other scripts involved in this hint. Save these in $HOME/Library/Libexec with the given names and make sure they are executable (chmod 755 name_of_script):

envx:


#!/usr/bin/python
import sys
import os
from shutil import copymode
from plistlib import Plist

def main(*args):
    path = os.path.join(os.getenv("HOME"), ".MacOSX/environment.plist")
    if os.path.isfile(path):
        environ = Plist.fromFile(path)
    else:
        environ = Plist()
    for arg in args:
        try: k, v = arg.split('=',1)
        except ValueError: pass
        else: environ[k] = v
    if not args:
        for k,v in environ.items():
            print "%s=\"%s\"; export %s" % (k, v, k)
    else:
        tmppath = path
        tmppath += ".tmp"
        f = open(tmppath, "w")
        environ.write(f)
        f.close()
        copymode(path, tmppath)
        os.rename(tmppath, path)

if __name__ == "__main__":
    main(*sys.argv[1:])
ssh-add:

#!/bin/sh
DISPLAY=bogus
SSH_ASKPASS=$HOME/Library/Libexec/ssh-askpass
export DISPLAY SSH_ASKPASS
/usr/bin/ssh-add < /dev/null
ssh-agent:

#!/bin/sh
eval `/usr/bin/ssh-agent -s`
$HOME/Library/Libexec/envx \
	SSH_AUTH_SOCK=$SSH_AUTH_SOCK \
	SSH_AGENT_PID=$SSH_AGENT_PID \
	SSH_ASKPASS=$HOME/Library/Libexec/ssh-askpass \
	DISPLAY=:0.0 \
	CVS_RSH=ssh
ssh-askpass:

#!/usr/bin/python
import sys
import os
import re

def decode_hex(s):
    s = eval('"' + re.sub(r"(..)", r"\x\1", s) + '"')
    if "\0" in s: s = s[:s.index("\0")]
    return s

def main(svce, acct):
    cmd = ' '.join([
        "/usr/bin/security",
        " find-generic-password",
        "-g -s '%s' -a '%s'" % (svce, acct),
        "2>&1 >/dev/null"
    ])
    p = os.popen(cmd)
    s = p.read()
    p.close()
    m = re.match(r"password: (?:0x([0-9A-F]+)\s*)?\"(.*)\"$", s)
    if m:
        hexform, stringform = m.groups()
        if hexform: print decode_hex(hexform)
        else: print stringform

if __name__ == "__main__":
    if len(sys.argv) == 3:
        main("SSH", sys.argv[2])
    else:
        main("SSH", os.getenv("USER"))


[ Reply to This | # ]
Create protected passwordless ssh logins - Part 3 of 3
Authored by: drudus on Nov 12, '10 08:32:42PM
I was wrestling with the 'security keychain-dump' outputting hex values & I found this hint.
There seems to be a simpler solution to convert from hex to ascii, use xxd.

eg… $ echo 0x68696e74730a | xxd -r -p
outputs 'hints' Hope someone finds it useful.

[ Reply to This | # ]
Create protected passwordless ssh logins - Part 3 of 3
Authored by: robertdb on Jul 12, '06 03:20:34AM
You gotto be kidding. Add this code to your ~/.profile and you are done:
variables=~/.ssh/variables

sshadd() {
 source "$variables" > /dev/null
 ssh-add -l > /dev/null 2>&1
 case "$?" in
  1)
   ssh-add > /dev/null 2>&1
  ;;
  2)
   rm "$variables"
   sshagent
  ;;
 esac
}

sshagent() {
 if [ -f "$variables" ] ; then
  sshadd
 else
  ssh-agent -s > $variables
  sshadd
 fi
}

sshagent
You will only need to enter your password once every reboot...

[ Reply to This | # ]