View Issue Details

IDProjectCategoryView StatusLast Update
0001193Main CAcert Websitecertificate issuingpublic2013-07-23 22:19
Reporterwijnen Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
PlatformFirefoxOSDebian GNU/LinuxOS Versionunstable
Summary0001193: patch for allowing domain verification using a HTTP probe instead of e-mail.
DescriptionAccording to (the bottom of) http://wiki.cacert.org/FAQ/NoDomainName, one of the methods to verify a domain is using a HTTP probe, but this is not currently implemented.
Steps To ReproduceLog in, go to domains->add, enter a domain, see that it only allows sending e-mails, not checking the content of the web site.
Additional InformationThe attached patch adds an option to the menu where an e-mail address must be selected. This option includes a hash. The user should create a text file containing this hash and place it on the web site as /cacert-probe.txt . When requesting to probe (clicking the button in the menu), the server will wget that file and check that it contains the hash. If it does, then the user has proven to be in control of the domain's web site and should be allowed to issue certificates for it (actually, according to the ruling they need to use an additional validation, but my main goal is to allow verification of domains without the ability to receive e-mail for them, so I hope this extra requirement will only be activated when more options are implemented).

Possible problems with the patch:
1. It may not work at all. I didn't see how I could easily set up a system to test it. I'm confident that it doesn't require major changes, however.
2. It's using wget to retrieve the file from the target webserver. This should work, but there might be a better way for doing this in php.

Non-issue:
Users may have access to some parts of a website, but not all of it. They must only be able to verify the domain if they have access over the top level documents (in particular, /~user/* is not enough). This patch only tries http at (protected) port 80 and only looks for /cacert-probe.txt . A regular user of the system will not be able to place that file on the server, and if no web server is running, will not be able to run a private web server on port 80.
TagsNo tags attached.
Reviewed by
Test Instructions

Activities

wijnen

2013-07-20 22:28

reporter  

http-probe.patch (2,540 bytes)   
diff --git a/includes/account.php b/includes/account.php
index 1a381b8..9c928cb 100644
--- a/includes/account.php
+++ b/includes/account.php
@@ -543,6 +543,7 @@
 				$addy[] = $sub;
 		$_SESSION['_config']['addy'] = $addy;
 		$_SESSION['_config']['domain'] = mysql_real_escape_string($newdomain);
+		$_SESSION['_config']['httphash'] = trim(make_hash());
 	}
 
 	if($process != "" && $oldid == 8)
@@ -553,6 +554,27 @@
 
 		$authaddy = trim(mysql_real_escape_string(stripslashes($_REQUEST['authaddy'])));
 
+		if($authaddy == "http hash")
+		{
+			// get site/cacert-probe.txt and check if it contains the hash.
+			$newhash = trim(`/usr/bin/wget -O- http://"$_SESSION['_config']['domain'].'/cacert-probe.txt'"`);
+			if($newhash != $_SESSION['_config']['httphash'])
+			{
+				showheader(_("My CAcert.org Account!"));
+				echo _("The hash could not be retrieved from http://".$_SESSION['_config']['domain'].'/cacert-probe.txt , or it did not match.');
+				showfooter();
+				exit;
+			}
+			$query = "insert into `domains` set
+					`domain`='".mysql_real_escape_string($_SESSION['_config']['domain'])."',
+					`memid`='".$_SESSION['profile']['id']."',
+					`created`=NOW(), `modified`=NOW(), `hash`=''";
+			mysql_query($query);
+			showheader(_("Updated"), _("Updated"));
+			echo _("Your domain has been verified. You can now start issuing certificates for this domain.<br>You can remove cacert-probe.txt from your website.");
+			exit;
+		}
+
 		if($authaddy == "" || !is_array($_SESSION['_config']['addy']))
 		{
 			showheader(_("My CAcert.org Account!"));
diff --git a/pages/account/8.php b/pages/account/8.php
index 79448d1..f25ba7f 100644
--- a/pages/account/8.php
+++ b/pages/account/8.php
@@ -19,7 +19,14 @@
 <table align="center" valign="middle" border="0" cellspacing="0" cellpadding="0" class="wrapper">
 
   <tr>
-    <td colspan="2" class="title"><?=_("Please choose an authority email address")?></td>
+    <td colspan="2" class="title"><?=sprintf(_("HTTP: please create a file named %s in the root of your web server containing only this key"), "cacert-probe.txt")?></td>
+  </tr>
+  <tr>
+    <td class="DataTD" width="75"><input type="radio" name="authaddy" value="http hash"<? if($tagged == 0) { echo " checked=\"checked\""; $tagged = 1; } ?> /></td>
+    <td class="DataTD" width="175"><?=$_SESSION['_config']['httphash']?></td>
+  </tr>
+  <tr>
+    <td colspan="2" class="title"><?=_("E-mail: please choose an authority email address")?></td>
   </tr>
 <? $tagged=0;
   if(is_array($_SESSION['_config']['addy']))
http-probe.patch (2,540 bytes)   

wijnen

2013-07-21 14:46

reporter  

http-probe-2.patch (2,613 bytes)   
diff --git a/includes/account.php b/includes/account.php
index 1a381b8..1659c0d 100644
--- a/includes/account.php
+++ b/includes/account.php
@@ -543,6 +543,7 @@
 				$addy[] = $sub;
 		$_SESSION['_config']['addy'] = $addy;
 		$_SESSION['_config']['domain'] = mysql_real_escape_string($newdomain);
+		$_SESSION['_config']['httphash'] = make_hash();
 	}
 
 	if($process != "" && $oldid == 8)
@@ -553,6 +554,27 @@
 
 		$authaddy = trim(mysql_real_escape_string(stripslashes($_REQUEST['authaddy'])));
 
+		if($authaddy == "http hash")
+		{
+			// get site/cacert-probe.txt and check if it contains the hash.
+			$newhash = file_get_contents('http://'.$_SESSION['_config']['domain'].'/cacert-probe.txt', NULL, NULL, NULL, strlen($_SESSION['_config']['httphash']));
+			if(strlen($newhash) == 0 || $newhash != $_SESSION['_config']['httphash'])
+			{
+				showheader(_("My CAcert.org Account!"));
+				echo _("The hash could not be retrieved from http://".$_SESSION['_config']['domain'].'/cacert-probe.txt , or it did not match.');
+				showfooter();
+				exit;
+			}
+			$query = "insert into `domains` set
+					`domain`='".mysql_real_escape_string($_SESSION['_config']['domain'])."',
+					`memid`='".$_SESSION['profile']['id']."',
+					`created`=NOW(), `modified`=NOW(), `hash`=''";
+			mysql_query($query);
+			showheader(_("Updated"), _("Updated"));
+			echo _("Your domain has been verified. You can now start issuing certificates for this domain.<br>You can remove cacert-probe.txt from your website.");
+			exit;
+		}
+
 		if($authaddy == "" || !is_array($_SESSION['_config']['addy']))
 		{
 			showheader(_("My CAcert.org Account!"));
diff --git a/pages/account/8.php b/pages/account/8.php
index 79448d1..f25ba7f 100644
--- a/pages/account/8.php
+++ b/pages/account/8.php
@@ -19,7 +19,14 @@
 <table align="center" valign="middle" border="0" cellspacing="0" cellpadding="0" class="wrapper">
 
   <tr>
-    <td colspan="2" class="title"><?=_("Please choose an authority email address")?></td>
+    <td colspan="2" class="title"><?=sprintf(_("HTTP: please create a file named %s in the root of your web server containing only this key"), "cacert-probe.txt")?></td>
+  </tr>
+  <tr>
+    <td class="DataTD" width="75"><input type="radio" name="authaddy" value="http hash"<? if($tagged == 0) { echo " checked=\"checked\""; $tagged = 1; } ?> /></td>
+    <td class="DataTD" width="175"><?=$_SESSION['_config']['httphash']?></td>
+  </tr>
+  <tr>
+    <td colspan="2" class="title"><?=_("E-mail: please choose an authority email address")?></td>
   </tr>
 <? $tagged=0;
   if(is_array($_SESSION['_config']['addy']))
http-probe-2.patch (2,613 bytes)   

wijnen

2013-07-21 14:49

reporter   ~0004173

I've uploaded a new version, which fixes two problems:

1. There was no check for an empty result. Since the hash couldn't be empty, this was no security issue, but now I'm checking it just to be sure.
2. wget doesn't limit the download size, so a DoS attack was possible by placing a giant file named /cacert-probe.txt at the server and probing it. Now I've used file_get_contents (which is better than using an external program anyway) and used its maxsize argument.

Uli60

2013-07-23 20:52

updater   ~0004182

per CPS 4.2.2 this is an allowed variant
https://www.cacert.org/policy/CertificationPracticeStatement.php#p4.2
section Domain verification

BenBE

2013-07-23 22:19

updater   ~0004186

Last edited: 2013-07-23 22:20

To avoid the situation where an "attacker" could search for all domains registered with CAcert by looking up a fixed filename.

Furthermore the filename should be based on details specific to the user account it is registered with, forcing revalidations of the domain after transfer to another account to fail.

Additionally we should allow administrators to authenticate requests from the CAcert servers by some domain or user specific token.

The used data might look like:

- $content = makehash();
- $filename = "cacert.".md5(salt.md5(uid).salt.makehash().salt.md5(domid)).".txt";
- $token = md5(salt.md5(uid).salt.md5(domid));
- $uri = http://$domain/$filename?auth=$token

This uses the auth parameter as a shared secret between CAcert and the webserver without neeing the server to act on the value of the auth parameter, thus making its interpretation optional.

Also the patch should check for the following things:
1. RFC-reserved IP address ranges should be declined
2. domains resolving into any of the CAcert internal networks should be declined and filtered by firewall
3. The method a domain was verified should be displayed

Issue History

Date Modified Username Field Change
2013-07-20 22:28 wijnen New Issue
2013-07-20 22:28 wijnen File Added: http-probe.patch
2013-07-21 14:46 wijnen File Added: http-probe-2.patch
2013-07-21 14:49 wijnen Note Added: 0004173
2013-07-23 20:52 Uli60 Note Added: 0004182
2013-07-23 22:19 BenBE Note Added: 0004186
2013-07-23 22:20 BenBE Note Edited: 0004186