Apache Won't Request My SSL Client Certificate - php

First off, please note that I am new to configuring SSL. In the past, I've always been fortunate to have an IT department to set that up for me ahead of time. So be prepared for the possibility that I might need to ask for clarification on some of your answers. =)
What I'm Trying to Do
I'm setting-up a company intranet website for employees. For example, there will be a browser start page that shows content customized for each employee. As such, I need to be able to identify said employee without requiring any username/password or other prompting (one-time setup is ok though; I just don't want them having to be prompted every time). Naturally, SSL would seem to be the best way to go about doing this.
I'll have a MySQL database setup to associate "user" accounts with SSL_CLIENT_M_SERIAL and SSL_CLIENT_I_DN, which I'm assuming will be unique for each client certificate(?). I got that idea from this article: http://cweiske.de/tagebuch/ssl-client-certificates.htm
The first time the user goes to the internal website, they won't have a certificate (I do NOT want to be generating them manually for clients!), in which case $_SERVER["SSL_CLIENT_VERIFY"] == "NONE". If that happens, it'll go to the user account setup page, which will include a step where PHP generates an SSL client certificate and sends it to the browser for the user to install. Nice and simple. The user then installs the cert, the association is made, and after restarting the browser (for good measure), the user goes back to the internal website.
At this point, Apache should request the client certificate, which the browser then sends. The PHP script then parses the necessary $_SERVER variables, compares against the MySQL database, and good times are had by all. Again, nice and simple.
What's Working So Far
I have the server-side certificates installed. And yes, they are self-signed (for obvious reasons). Apache has mod_ssl installed and all that seems to be working fine. I created a PHP script that just dumps the $_SERVER array, and all the SSL_SERVER_* key values match the certificate I created for it.
The Problem
I can't get the client certificates to work! In that same PHP script, no matter what I do, SSL_CLIENT_VERIFY == "NONE" and the other SSL_CLIENT_* keys are missing. This is what happens if I have SSLVerifyClient set to optional in ssl.conf. According to every tutorial I've read, they all say that the webserver should ask the browser for a client certificate. Thing is, I can't get it to do that! It just goes straight to the PHP script and assumes I have no client certificates at all. This happens in Firefox, Chrome, and IE.
So I tried setting SSLVerifyClient to required and restarted the webserver. With that option in place, I can't even establish an SSL connection. Firefox just says the connection has been reset (other browsers display their own versions of that error as well). What's weird is that the logs don't show ANY activity on these connection attempts! I.e. access_log, error_log, ssl_access_log, ssl_error_log, AND ssl_request_log all don't show ANYTHING; it's as if the attempt never even occurred. This is frustrating because it means I don't even have an error message to work from. Just a webserver passive-aggressively telling me to go to hell.
I tried generating/installing my own client certificate manually using PHP's OpenSSL extension. The certificate installed just fine, though I can't find any information on how to associate that certificate with the server one (assuming I even need to?). Also, it doesn't seem to matter anyway, because Apache still won't even request a client certificate if optional is set. And if require is set, it just blows up without explanation. I need it to be set to optional anyway for this schema to work.
The Environment
OS: CentOS 5.7 64-bit (VirtualBox)
Apache: 2.2.3
PHP: 5.3.10
I'm guessing you might need more info to help me, so please ask! I'll provide you with whatever you need.
To summarize, I need to know how to get Apache to request an SSL client certificate given the conditions outlined above. Also, if there's any special signing/etc that has to be done to make the client certificate "compatible" with the server certificate (again, WITHOUT doing so manually via shell for each client cert!), I'll need to know that as well.
I am 100% stuck on this as of now. Can't find anything even remotely helpful on Google. Any help you can provide on this would be TREMENDOUSLY appreciated!! Thanks! =)

First, you need to configure Apache Httpd to request a client certificate. For this, you need at least to use SSLVerifyClient optional on the location/directory you want to be authenticated with this method.
Secondly, the certificates sent by the client needs to be trusted by Apache Httpd too. (You could in principle use SSLVerifyClient optional_no_ca, let any client cert through at the Apache Httpd SSL/TLS stack, and only then verify the certificate within PHP, but that's quite a bit of work, for which you need be a bit more careful since that's not necessarily easy code; more importantly, this would be quite useless in this context, since you're in a scenario where you're in control of your CA.)
As far as I understand, SSL_CLIENT_VERIFY (a variable that I haven't used much myself) seems only really useful with the SSLVerifyClient optional_no_ca. It might work with SSLVerifyClient optional, but I doubt so. SSLVerifyClient require will reject connections using a client certificate that is not trusted (by one of the CAs in SSLCACertificateFile/SSLCACertificatePath), or if there is no certificate. As far as I know, SSLVerifyClient optional will let the client through without a certificate or with a trusted certificate, but will also reject the connection if the certificate is not trusted.
Here, by rejecting the connection, I mean closing the SSL/TLS connection abruptly with an alert. There is no chance to produce an HTTP(S) error page. All you'll get in the standard browser error, something along the lines of ssl_error_unknown_certificate_.... (You should consider this in terms of usability.)
From then onwards, what you need is to set up your own CA, possibly web-based with in-browser key-generation and within the same website. You wouldn't want SSLVerifyClient require for that, because you would need to let the users who don't have a certificate yet in (use optional instead). This being said, these directives need not apply to the entire host, but can be specific to certain locations/directories.
Integrating your own web-based CA (or more generally, creating your own CA) isn't necessarily easy if you're new to all this. Ready-made tools exist (e.g. OpenCA), or you can build your own using various bits of JavaScript/ActiveX, and you would need the server-side code to handle the SPKAC or PKCS#10 requests (and to issue the actual certificate). (For such a CA to be useful, you'd want the users who apply for a new certificate to provide some proof of ID at the time of application, perhaps a password.)
When this is set up, you should configure SSLCACertificateFile (or ...Path) to point to the CA certificate of your internal CA (whether it's a web-based CA or not, on the same site or not). (Of course, keep the private key of your CA private, perhaps configured within your CA web-based application, but Apache Httpd itself doesn't need to know about it.) Browsers will only suggest certificates issued by those CAs or intermediates (unless you've also configured SSLCADNRequestFile, which would be used to send the list of accepted CAs instead).
Note that these two steps (setting up your CA and setting up your website to use client-certificates) really are independent in fact. The fact that both can be part of the same site can be convenient, but isn't necessary. You could try out the Apache Httpd set up without deploying an entire CA on the site first (I'd recommend that, even if it's just to see what you're getting into). There are a number of tools to create your own little CA that are manageable with a handful of certificates: OpenSSL's CA.pl or TinyCA for example. You could also use these test certificates (localhost and testclient, testclient_r is revoked if you want to use the CRL, probably not necessary at first): all passwords are testtest.
As you've already anticipated (with your MySQL DB), you'll need to manage the certificates you issue and map them to users. SSL_CLIENT_M_SERIAL and SSL_CLIENT_I_DN are not the right variables to use, though. SSL_CLIENT_I_DN is the Issuer DN (i.e. the CA's Subject DN). What you'd be looking for is SSL_CLIENT_S_DN: the client cert Subject DN. SSL_CLIENT_M_SERIAL is the certificate serial number: don't use it, since it's unique per certificate: one user could have multiple certificates with the same Subject DN (e.g. if one expires or is revoked).
Despite all this, I'm not sure whether client-certificates are the best way to achieve your goal (letting the employees in your company log on without password).
Firstly, the user should protect their own certificates with a password anyway. What you're really after is some form of Single-Sign On (SSO), I guess.
Secondly, depending on the degree of computer-literacy of your users, certificates can actually be quite difficult to manage.
The fact that the word "certificate", strictly speaking, doesn't include the private key at all, but sometimes implies usage of the private key can be confusing for some. On the one hand, you sometimes hear "Import your certificate into your browser" and "Use your certificate to log in"; on the other hand, you can also hear "send me your certificate". The former implies usage and availability of the private key ("certificate" might just mean .p12 in these expressions). The latter definitely shouldn't involve the private key.
Browser user interfaces tend to be quite poor or confusing for managing the certificates or logging out. Again, if the certificate isn't recognised, the SSL/TLS connection will not be established, so the web server doesn't get a chance to display an HTML error page of any sort.
Perhaps you could also consider other forms of SSO (e.g. CAS, something SAML-based or Kerberos/SPNEGO.)

I have a similar problem with:
CentOS 6.3
Apache 2.2.15
After some tries i recognize my problem.
If I set SSLVerifyClient optional or SSLVerifyClient optional_no_ca and I specify also SSLCACertificateFile or SSLCACertificatePath, Apache acquires the client certificate only if it's released from CA found in the CA reference file/path specified in configuration.

You may have a look to the apache doc if not done already.
The general principle is that you create your self-signed cert and check it before trying to use it.
Then it looks like the client connects to your intranet site through http. From there, there are many different ways to switch to https using your ssl cert. The easiest way is to use the apache rewrite module. But in your case, as you are making php/mysql checks, you may redirect your client from http to to https, which is not the simple way.
In any of both cases (apache automatic redirect through mod_rewrite, or redirection by cascading tests (php/javascript/html), you need to set up your 2 vhosts (one for http and one for https) in the proper way, but this assumes some hypothesis.
For example (debian - apache 2.2), here is an automatic redirect, done by Apache (eg 1st case described above) :
cat /etc/apache2/sites-available/test
# VHOST test
<VirtualHost *:80>
DocumentRoot /home/www/test
ServerName www.test.dev
# ######################
# Redirect commons
# ######################
RewriteEngine on
# Case of vhosts
RewriteOptions Inherit
# ######################
# Redirect (empty index)
# ######################
# Condition 1 to redirect : request matching with the server
RewriteCond %{HTTP_HOST} ^www\.test\.dev [NC]
# Condition 2 to redirect : non empty HOST
RewriteCond %{HTTP_HOST} !^$
# Automatic Empty requests Redirect
RewriteRule ^(.*)/$ /index.php
# ######################
# Redirect to SSL
# ######################
RewriteCond %{HTTP_HOST} ^www\.test\.dev [NC]
RewriteCond %{HTTP_HOST} !^$
RewriteCond %{SERVER_PORT} ^80$
RewriteCond %{REQUEST_URI} /
# Redirection
RewriteRule ^/(.*)$ https://%{SERVER_NAME}%{REQUEST_URI} [L,R]
</VirtualHost>
The second virtual host for SSL :
cat /etc/apache2/sites-available/test-ssl
# VHOST for ssl
DocumentRoot "/home/www/test"
ServerName www.test.dev
# SSL
SSLEngine on
SSLCACertificateFile /etc/apache2/ssl/cur_cert/ca.pem
SSLCertificateFile /etc/apache2/ssl/cur_cert/serveur.pem
SSLCertificateKeyFile /etc/apache2/ssl/cur_cert/serveur.key
<Directory "/home/www/test">
Options FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
Allow from 127.0.0.1 192.168.0.0/16
</Directory>
<Directory "/home/www/test/cgi-bin">
Options FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
Allow from 127.0.0.1 192.168.0.0/16
Options +ExecCGI
AddHandler cgi-script .cgi
</Directory>
</VirtualHost>
Your case might defer slightly from this, eg you will not have the redirect portion in the 1st vhost, but only a simple vhost and the second one for https (ssl). The redirection will be done by php/javascript once you have achieved your mysql checks.
Here is an example abstract from a php class for the way to cascade the switch from http to https, using php, then javascript, then html :
public function Redirect($url){
if (TRUE !== Validator::isValidURL($url))
die ("FATAL ERR: url not valid");
// PHP ABSOLUTE URL REDIRECT (HTTP1.1)
if (!headers_sent()) {
header("Status: 200");
header("Cache-Control: no-cache, must-revalidate"); // required for HTTP/1.1
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // past Date
header("Pragma: no-cache");
header('Location: '.$url); // note: 302 code return by default with "Location"
flush();
exit();
// if headers are already sent... do javascript redirect... if javascript is disabled, do html redirect.
} else {
// Js redirect
echo '<script type="text/javascript">';
//echo "<!--";
echo 'document.location="'. $url .'";';
//echo "//-->";
echo '</script>';
// HTML redirect if js disabled
echo '<noscript>';
echo '<meta http-equiv="refresh" content="0;url="'.$url.'" />';
echo '</noscript>';
exit();
}
return FALSE;
} /* end of method (redirect) */
Hope it helps you to better understand how to proceed and adapt this approach to your specific case.

Related

Disallow HTTPS traffic from .htaccessfile

I've got a client site hosted on PagodaBox (Laravel 4) and it is serving https request with a certificate warning. The client's internal applications expect any https traffic to fail on the web host and follow through to their internal gateway.
Is it possible to disallow all HTTPS request from within an htaccess file? Redirecting will not work in this case. We cannot modify the httpd.conf so it looks like .htaccess is the only option.
So far, I've tried this and the site will still accept HTTPS request:
Options +FollowSymLinks
RewriteEngine On
RewriteCond %{HTTPS} off
This will never work. The SSL connection has to be established FIRST, before the HTTP layer is ever processed. That means your users will STILL get a certificate warning, and then get redirected.
In real world terms, you've got a box giftwrapped with razor wire. They have to get through the razor wire (your cert warnings) so they can see that box contains a note saying "present is under bed".
Your rewrite is basically saying "If https is off, then do nothing". You have no RewriteRule, which is where the actual rewriting occurs. And since on an SSL-enabled site HTTPS would never be off, the RewriteCond would never match to start with.
You can't redirect from an SSL connection witch has no valid certificate. The redirect comes after the page to accept the untrusted certificate.
Without accepting the untrusted certificate you have no valid connection.

How to keep php site from loading outside HTTPS [duplicate]

So I want to force the user to access the https version of my page rather than the http. And according to this post all I have to do is this:
RewriteEngine On
RewriteCond %{SERVER_PORT} 80
RewriteRule ^(.*)$ https://www.yourdomain.com/$1 [R,L]
But MY site resides in a folder within the main directory, so it's in www.domain.com/Folder. So should this htaccess code go inside the main directory or in the subdirectory. Because I do not want to change the way the access the main site, only the folder.
This is a not-so-good method of going about this, especially if you have access to httpd.conf. The better method is to create TWO virtual hosts. One for your standard port 80 stuff, which simply has an unconditional redirect to the SSL version, e.g. in pseudo-ish .conf talk:
<VirtualHost example.com:80>
RedirectPermanent / https://example.com
DocumentRoot /some/fake/path
</VirtualHost>
<VirtualHost example.com:443>
normal site stuff here...
</VirtualHost>
This has the advantage of leaving the redirect viable even if a config messup disables .htaccess files, plus serving up bogus/non-existent content if SSL dies for whatever reason.
You can leave it in the root directory but change it to:
RewriteRule ^(your-directory/.*)$ https://www.yourdomain.com/$1 [R,L]
Keep in mind, though, that before the redirect happens, the cookies and query parameters with possibly sensitive data has already been sent in clear text, so remember to use the secure cookie atribute if you use cookies.
Your site can be vulnerable if you're redirecting from http to https. Take a look at this for some more information on that.
http://www.thoughtcrime.org/software/sslstrip/
seems silly to "force ssl" till they fix the big gaping security hole it opens up in browsers in the name of "site verification"
this has no real basis and there is potential for abuse by a rogue CA, rogue state, or corruption.
(and the "verification" is useless anyway not being based on user wishes not anyone actually looking at the sites - there are plenty of phishing sites out there with "valid" certificates!)
there is way too much misinformation being bandied around about SSL
you get the same encryption with a self signed certificate but browsers tell users you site is "untrusted" (with of course no basis - "not checked" or "not verifiable" would be what any warning should actually say - warnings need to be informative not something that just scares users so much most of them just close them without even reading the rest of the warning!)
until this is fixed in browsers I cannot recommend the use of SSL at all in a web site context.
meanwhile all I can recommend to forget port 443 and implement your own encryption layer (or use something like ssh if it doesn't need to be a browser)

Apache's mod_proxy_html with PHP header("Location:..."); doesn't redirect

I've googled the crap out of this problem and came up with nothing, so hopefully you guys can help.
Goal
Configure a reverse proxy (using Apache's mod_proxy) to enable access to an internal PHP application over the internet using mod_proxy_html to rewrite URLs in my application.
Problem Description
Consider landing.php with only this code:
redirect.php
and redirect.php with only:
<?php
header("Location:http://internal.example.com/landing.php");
?>
with this snippet from my httpd.conf
UseCanonicalName On
LoadFile /usr/lib64/libxml2.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule xml2enc_module modules/mod_xml2enc.so
<VirtualHost *:80>
ServerName example.com
<Proxy *>
Order deny,allow
Allow from all
AllowOverride None
</Proxy>
ProxyPass / http://internal.example.com/
ProxyPassReverse / http://internal.example.com/
ProxyHTMLLinks a href #Commenting out this line fixes the problem, but I need it for rewriting
ProxyHTMLEnable On
RequestHeader unset Accept-Encoding
</VirtualHost>
When I go to http://example.com/landing.php and click "redirect.php" it should take me back to the landing.php page. Instead, I get a "This connection was reset" in Firefox, or "No data received" in Chrome. (FYI, going to http://internal.example.com/redirect.php redirects correctly.)
The Question:
Why the redirect would fail going through the reverse proxy, and how can I fix this?
Hints
I've discovered a few things that could be helpful...
I know that if I comment out "ProxyHTMLLinks a href", this will work correctly. But obviously, this is the rewrite functionality I need.
I can also change the redirect.php page to the following, this works correctly:
<?php
header("Location:http://internal.example.com/landing.php");
?>
random text
I guess this text somehow does something to the page or HTTP headers that make mod_proxy_html (or more specifically ProxyHTMLLinks) operate differently than without it.
I can also change the redirect.php page to the following and have it work:
<?php
header("Location:http://internal.example.com/landing.php");
header("Content-Type:");
?>
This works because ProxyHTMLLinks, by default, is only applied to Content-Type text/html files. However, I don't want to have to hack all calls to header("Location:...") to make this work. I don't mind changing all the calls to header("Location:..."), assuming what I'm changing is correcting a problem, not creating a hack.
Lastly, I've done some packet sniffing on the reverse proxy server and discovered that the header("Location:...") sends a HTTP/1.1 302 Not Found to the reverse proxy server, but it doesn't pass this through to the browser requesting redirect.php. When I try one of the "solutions" above, the 302 is then passed from the reverse proxy server to the computer requesting redirect.php.
My understanding is that the Location header should go to the browser, and then the browser should request the new location passed back. So it is failing because the 302 doesn't make it to the browser...
FYI, I've tried looking at the error logs to see if mod_proxy_html is failing somewhere, but I don't see anything, though I'm open to specific suggestions with regards to logging, since I'm not 100% sure if I'm setting the logging up correctly.
Sorry this is so long, just trying to be as specific as possible.
Thanks in advance!
I figured out the problem. I needed to explicitly pass the charset in the header Content-Type for this to work.
This was accomplished by adding:
AddDefaultCharset utf-8
to my Apache config file. This globally fixed all calls to header("Location:...") without having to add header("Content-Type:") or header("Content-Type:text/html;charset=utf-8") to each one of them.
In short, what I'm saying that the mod_proxy_html's ProxyHTMLLinks causes a 302 Found to not be forwarded from the reverse proxy server to the client if a) the content-type is text/html (and thus ProxyHTMLLinks) applies, b) the charset is not set and c) your page has no content passed back.
In my opinion, this is a normal scenario. Pages which process form inputs often meet all three criteria.
It's possible that for some reason this is the intended functionality, and that I'm doing something else wrong, but I can't see what that would be. At least there is an elegant workout here in case anyone finds it useful.

When could the HTTP Host header be undefined?

According to RFC 2616, which defines HTTP/1.1, the Host: header is mandatory.
A client MUST include a Host header field in all HTTP/1.1 request messages .
But the PHP manual implies that it could be empty:
'HTTP_HOST': Contents of the Host: header from the current request, if there is one.
In which situations could this header, and thus $_SERVER['HTTP_HOST'], be empty? Could my application depend on its being there?
It can be empty in HTTP 1.0. If no host header is specified, virtual hosting won't work at all, so the default vhost in your web server will be used.
I just tested this myself; in PHP under Nginx the $_SERVER['HTTP_HOST'] variable got set to the name of the virtual host, which is _ in my case. But that also depends on your fastcgi_params configuration in Nginx.
On shared hosting this is not important since the default vhost will be set to some information page from the hosting company, and so your script will not be run. Could be a good thing to keep in mind for your own server though.
Crawlers (e.g. google), scrapers or even perfectly legal scripts interfacing with your API may accidentally or ignorantly skip the Host header.
I added this answer because this question came up on google when I looked for the same thing.

How to enable and use HTTP PUT and DELETE with Apache2 and PHP?

It should be so simple. I've followed every tutorial and forum I could find, yet I can't get it to work. I simply want to build a RESTful API in PHP on Apache2.
In my VirtualHost directive I say:
<Directory />
AllowOverride All
<Limit GET HEAD POST PUT DELETE OPTIONS>
Order Allow,Deny
Allow from all
</Limit>
</Directory>
Yet every PUT request I make to the server, I get 405 method not supported.
Someone advocated using the Script directive, but since I use mod_php, as opposed to CGI, I don't see why that would work.
People mention using WebDAV, but to me that seems like overkill. After all, I don't need DAV locking, a DAV filesystem, etc. All I want to do is pass the request on to a PHP script and handle everything myself. I only want to enable PUT and DELETE for the clean semantics.
You don't need to configure anything. Just make sure that the requests map to your PHP file and use requests with path info. For example, if you have in the root a file named handler.php with this content:
<?php
var_dump($_SERVER['REQUEST_METHOD']);
var_dump($_SERVER['REQUEST_URI']);
var_dump($_SERVER['PATH_INFO']);
if (($stream = fopen('php://input', "r")) !== FALSE)
var_dump(stream_get_contents($stream));
The following HTTP request would work:
Established connection with 127.0.0.1 on port 81
PUT /handler.php/bla/foo HTTP/1.1
Host: localhost:81
Content-length: 5
 
boo
HTTP/1.1 200 OK
Date: Sat, 29 May 2010 16:00:20 GMT
Server: Apache/2.2.13 (Win32) PHP/5.3.0
X-Powered-By: PHP/5.3.0
Content-Length: 89
Content-Type: text/html
 
string(3) "PUT"
string(20) "/handler.php/bla/foo"
string(8) "/bla/foo"
string(5) "boo
"
Connection closed remotely.
You can hide the "php" extension with MultiViews or you can make URLs completely logical with mod_rewrite.
See also the documentation for the AcceptPathInfo directive and this question on how to make PHP not parse POST data when enctype is multipart/form-data.
AllowOverride AuthConfig
try this. Authentication may be the problem. I was working with a CGI script written in C++, and faced some authentication issues when passed DELETE. The above solution helped me. It may help in your case too.
Also even if you don't get the solution for your problem of PUT and DELETE, do not stop working rather use "CORS". It is a google chrome app, which will help you bypass the problem, but remember it is a temporary solution, so that your work or experiments doesn't remain freeze for long. Obviously, you cannot ask your client to have "CORS" enabled to run your solution, as it may compromise systems security.
On linux, /etc/apache2/mods-enabled/php5.conf dans php5.load exists. If not, enables this modules (may require to sudo apt-get install libapache2-mod-php5).
IIRC the purpose of the form method attribute was to define different transport methods. Consequently, HTML 5.2 only defines GET, POST, and DIALOG methods for transport and dialog action, not how the server should process the data.
Ruby-on-rails solves this problem by using POST/GET for everything and adding a hidden form variable that defines the actual ReST method. This approach is more clumsy and error-prone, but does remove the burden from both the HTML standard and browser developers.
The form method was defined before ReST, so you cannot define ReST in HTML, even after enabling Apache and PHP because the browsers conform to HTML and therefore default to GET/POST for all non-HTML defined values. That means, when you send a form to the browser with a PUT method, the browser changes that to GET and uses that instead. The hidden variable, however, passes through everything unchanged, so you can use that to customise your form handling process.
Hope that helps
The technical limitations with using PUT and DELETE requests does not lie with PHP or Apache2; it is instead on the burden of the browser to sent those types of requests.
Simply putting <form action="" method="PUT"> will not work because there are no browsers that support that method (and they would simply default to GET, treating PUT the same as it would treat gibberish like FDSFGS). Sadly those HTTP verbs are limited to the realm of non-desktop application browsers (ie: web service consumers).

Categories