1<?php
2/**
3 * Copyright 2013-2017 Horde LLC (http://www.horde.org)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you did
6 * not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @author   Michael J Rubinsky <mrubinsk@horde.org>
9 * @category Horde
10 * @license  http://www.horde.org/licenses/lgpl21 LGPL-2.1
11 * @package  Auth
12 * @since 2.1.0
13 */
14
15/**
16 * The Horde_Auth_X509 class provides an authentication driver for using X509
17 * client certificates. Since X509 certificates do not provide the password,
18 * if the server setup requires the use of per-user passwords, a callback
19 * function may be passed to obtain it from.
20 *
21 * @author    Michael J Rubinsky <mrubinsk@horde.org>
22 * @category  Horde
23 * @copyright 2013-2017 Horde LLC
24 * @license   http://www.horde.org/licenses/lgpl21 LGPL-2.1
25 * @package   Auth
26 * @since     2.1.0
27 */
28class Horde_Auth_X509 extends Horde_Auth_Base
29{
30    /**
31     * An array of capabilities, so that the driver can report which
32     * operations it supports and which it doesn't.
33     *
34     * @var array
35     */
36    protected $_capabilities = array(
37        'transparent' => true
38    );
39
40    /**
41     * Constructor.
42     *
43     * @param array $params  Parameters:
44     *  - password: (string) If available, the password to use for the session.
45     *    DEFAULT: no password used.
46     *  - username_field: (string) Name of the $_SERVER field that
47     *    the username can be found in. DEFAULT: 'SSL_CLIENT_S_DN_EMAILADDRESS'.
48     *  - certificate_field: (string) Name of the $_SERVER field that contains
49     *    the full certificate. DEFAULT: 'SSL_CLIENT_CERT'
50     *  - ignore_purpose: (boolean) If true, will ignore any usage restrictions
51     *    on the presented client certificate. I.e., if openssl_x509_checkpurpose
52     *    returns false, authentication may still proceed. DEFAULT: false - ONLY
53     *    ENABLE THIS IF YOU KNOW WHY YOU ARE DOING SO.
54     *  - filter: (array)  An array where the keys are field names and the
55     *                     values are the values those certificate fields MUST
56     *                     match to be considered valid. Keys in the format of
57     *                     fieldone:fieldtwo will be taken as parent:child.
58     *                     DEFAULT: no additionachecks applied.
59     *
60     * @throws InvalidArgumentException
61     */
62    public function __construct(array $params = array())
63    {
64        $params = array_merge(array(
65            'password' => false,
66            'username_field' => 'SSL_CLIENT_S_DN_CN',
67            'certificate_field' => 'SSL_CLIENT_CERT',
68            'ignore_purpose' => true,
69            'filter' => array()
70        ), $params);
71
72        parent::__construct($params);
73    }
74
75    /**
76     * Not implemented.
77     *
78     * @param string $userId      The userID to check.
79     * @param array $credentials  An array of login credentials.
80     *
81     * @throws Horde_Auth_Exception
82     */
83    protected function _authenticate($userId, $credentials)
84    {
85        throw new Horde_Auth_Exception('Unsupported.');
86    }
87
88    /**
89     * Automatic authentication: checks if the username is set in the
90     * configured header.
91     *
92     * @return boolean  Whether or not the client is allowed.
93     */
94    public function transparent()
95    {
96        if (!is_callable('openssl_x509_parse')) {
97            throw new Horde_Auth_Exception('SSL not enabled on server.');
98        }
99
100        if (empty($_SERVER[$this->_params['username_field']]) ||
101            empty($_SERVER[$this->_params['certificate_field']])) {
102            return false;
103        }
104
105        // Valid for client auth?
106        $cert = openssl_x509_read($_SERVER[$this->_params['certificate_field']]);
107        if (!$this->_params['ignore_purpose'] &&
108            !openssl_x509_checkpurpose($cert, X509_PURPOSE_SSL_CLIENT) &&
109            !openssl_x509_checkpurpose($cert, X509_PURPOSE_ANY)) {
110            return false;
111        }
112
113        $c_parsed = openssl_x509_parse($cert);
114        foreach ($this->_params['filter'] as $key => $value) {
115            $keys = explode(':', $key);
116            $c = $c_parsed;
117            foreach ($keys as $k) {
118                $c = $c[$k];
119            }
120            if ($c != $value) {
121                return false;
122            }
123        }
124
125        // Handle any custom validation added by sub classes.
126        if (!$this->_validate($cert)) {
127            return false;
128        }
129
130        // Free resources.
131        openssl_x509_free($cert);
132
133        // Set credentials
134        $this->setCredential('userId', $_SERVER[$this->_params['username_field']]);
135        $cred = array('certificate_id' => $c_parsed['hash']);
136        if (!empty($this->_params['password'])) {
137            $cred['password'] = $this->_params['password'];
138        }
139        $this->setCredential('credentials', $cred);
140
141        return true;
142    }
143
144    /**
145     * Perform additional validation of certificate fields.
146     *
147     * @return boolean
148     */
149    protected function _validate($certificate)
150    {
151        return true;
152    }
153
154}
155