Source code for gateone.auth.ctypes_pam

# -*- coding: utf-8 -*-
# Original version (pam-0.1.3) ©2007 Chris AtLee <chris@atlee.ca>
# This version (modifications) © 2013 Liftoff Software Corporation
# Licensed under the MIT license:
#   http://www.opensource.org/licenses/mit-license.php
# This is a modified version of pam-0.1.3 that adds support for
# pam_set_item (specificallly, to support setting a PAM_TTY)

# Meta
__license__ = "MIT"
__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'

__doc__ = """\
.. _gopam.py:

PAM Authentication Module for Python
====================================
Provides an authenticate function that will allow the caller to authenticate
a user against the Pluggable Authentication Modules (PAM) on the system.

Implemented using ctypes, so no compilation is necessary.
"""

__all__ = ['authenticate']

from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, pointer, sizeof
from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int
from ctypes.util import find_library

LIBPAM = CDLL(find_library("pam"))
LIBC = CDLL(find_library("c"))

CALLOC = LIBC.calloc
CALLOC.restype = c_void_p
CALLOC.argtypes = [c_uint, c_uint]

STRDUP = LIBC.strdup
STRDUP.argstypes = [c_char_p]
STRDUP.restype = POINTER(c_char) # NOT c_char_p !!!!

# Various constants
PAM_PROMPT_ECHO_OFF = 1
PAM_PROMPT_ECHO_ON = 2
PAM_ERROR_MSG = 3
PAM_TEXT_INFO = 4
# pam_set_item and pam_get_item constants:
PAM_SERVICE =       1    # The service name
PAM_USER =          2    # The user name
PAM_TTY =           3    # The tty name
PAM_RHOST =         4    # The remote host name
PAM_CONV =          5    # The pam_conv structure
PAM_AUTHTOK =       6    # The authentication token (password)
PAM_OLDAUTHTOK =    7    # The old authentication token
PAM_RUSER =         8    # The remote user name
PAM_USER_PROMPT =   9    # the prompt for getting a username
# These are Linux-specific pam_set_item/pam_get_item constants:
PAM_FAIL_DELAY =   10    # app supplied function to override failure
PAM_XDISPLAY =     11    # X display name
PAM_XAUTHDATA =    12    # X server authentication data
PAM_AUTHTOK_TYPE = 13    # The type for pam_get_authtok

class PamHandle(Structure):
    """wrapper class for pam_handle_t"""
    _fields_ = [("handle", c_void_p)]

    def __init__(self):
        Structure.__init__(self)
        self.handle = 0

class PamMessage(Structure):
    """wrapper class for pam_message structure"""
    _fields_ = [("msg_style", c_int), ("msg", c_char_p)]

    def __repr__(self):
        return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)

class PamResponse(Structure):
    """wrapper class for pam_response structure"""
    _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]

    def __repr__(self):
        return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)

CONV_FUNC = CFUNCTYPE(
    c_int,
    c_int,
    POINTER(POINTER(PamMessage)),
    POINTER(POINTER(PamResponse)),
    c_void_p)

class PamConv(Structure):
    """wrapper class for pam_conv structure"""
    _fields_ = [("conv", CONV_FUNC), ("appdata_ptr", c_void_p)]

PAM_START = LIBPAM.pam_start
PAM_START.restype = c_int
PAM_START.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]

PAM_AUTHENTICATE = LIBPAM.pam_authenticate
PAM_AUTHENTICATE.restype = c_int
PAM_AUTHENTICATE.argtypes = [PamHandle, c_int]

PAM_SET_ITEM = LIBPAM.pam_set_item
PAM_SET_ITEM.restype = c_int
PAM_SET_ITEM.argtypes = [PamHandle, c_int, c_char_p]

[docs]def authenticate(username, password, service='login', tty="console", **kwargs): """ Returns True if the given username and password authenticate for the given service. Returns False otherwise. :param string username: The username to authenticate. :param string password: The password in plain text. :param string service: The PAM service to authenticate against. Defaults to 'login'. :param string tty: Name of the TTY device to use when authenticating. Defaults to 'console' (to allow root). If additional keyword arguments are provided they will be passed to PAM_SET_ITEM() like so:: PAM_SET_ITEM(handle, <keyword mapped to PAM_whatever>, <value>) Where the keyword will be automatically converted to a PAM_whatever constant if present in this file. Example:: authenticate(user, pass, PAM_RHOST="myhost") ...would result in:: PAM_SET_ITEM(handle, 4, "myhost") # PAM_RHOST (4) taken from the global """ encoding = 'utf-8' if not isinstance(password, bytes): password = password.encode(encoding) if not isinstance(username, bytes): username = username.encode(encoding) if not isinstance(service, bytes): service = service.encode(encoding) if not isinstance(tty, bytes): tty = tty.encode(encoding) @CONV_FUNC def my_conv(n_messages, messages, p_response, app_data): """ Simple conversation function that responds to any prompt where the echo is off with the supplied password. """ # Create an array of n_messages response objects addr = CALLOC(n_messages, sizeof(PamResponse)) p_response[0] = cast(addr, POINTER(PamResponse)) for i in range(n_messages): if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF: pw_copy = STRDUP(password) p_response.contents[i].resp = cast(pw_copy, c_char_p) p_response.contents[i].resp_retcode = 0 return 0 handle = PamHandle() conv = PamConv(my_conv, 0) retval = PAM_START(service, username, pointer(conv), pointer(handle)) PAM_SET_ITEM(handle, PAM_TTY, tty) for key, value in kwargs.items(): if key.startswith('PAM_') and key in globals(): if isinstance(value, str): value = value.encode(encoding) PAM_SET_ITEM(handle, globals()[key], value) if retval != 0: # TODO: This is not an authentication error, something # has gone wrong starting up PAM return False retval = PAM_AUTHENTICATE(handle, 0) return retval == 0
def pam_service_exists(service): """ Returns ``True`` if the given *service* can be found in the system's PAM configuration. """ if os.path.isdir('/etc/pam.d'): # Modern PAM implementation. Services are named after files. if service in os.listdir('/etc/pam.d/'): return True else: # Old-school PAM implementation (Solaris, AIX, etc). services = [] # He's making a list, and checkin' it twice. for line in open('/etc/pam.conf'): if line.startswith('#'): # It's a comment continue _service = line.split()[0] if _service not in services: services.append(_service) if service in services: return True return False if __name__ == "__main__": # Do a little test. Make a little love. Get down tonight! import os, sys, getpass print("\x1b[1mTesting PAM authentication\x1b[0m") if os.getuid() != 0: print( # Print in bold/yellow "\x1b[1;33mWarning: You're not root. This means you'll only be " "able to test authenticating your own user ({0}).\x1b[0m" .format(getpass.getuser())) service = raw_input("PAM Service [login]: ") if not service: service = 'login' if not pam_service_exists(service): print( "\x1b[1;33mWarning: The given service, '{0}' could not be found in " "this system's PAM configuration. This means the 'other' service " "will be used.\x1b[0m".format(service)) getting_user = True while getting_user: user = raw_input("Username [{0}]: ".format(getpass.getuser())) getting_user = False if not user: user = getpass.getuser() if os.getuid() != 0 and user != getpass.getuser(): getting_user = True print( "ERROR: I told you that you can only authenticate as yourself " "(since you're not root).") print( "Try again but this time just hit the enter key or actually " "type out your own username.") password = getpass.getpass() try: result = authenticate(user, password) if result: print("SUCCESS: PAM authentication definitely works.") else: print( "FAIL: Authentication failed. Did you enter your password " "correctly?") print( "If this keeps happening you either need some caffeine or you " "need to check the system logs to see why the authentication " "is failing.") except Exception as e: print("EPIC FAIL: Something horrible went wrong. Exception message:") print(e) print("Here's the traceback:") import traceback traceback.print_exc(file=sys.stdout)