summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Auth/OpenID/TrustRoot.php193
-rw-r--r--Tests/Auth/OpenID/TrustRoot.php171
-rw-r--r--Tests/Auth/OpenID/data/trustroot.txt115
-rw-r--r--Tests/TestDriver.php3
4 files changed, 481 insertions, 1 deletions
diff --git a/Auth/OpenID/TrustRoot.php b/Auth/OpenID/TrustRoot.php
new file mode 100644
index 0000000..66a73ec
--- /dev/null
+++ b/Auth/OpenID/TrustRoot.php
@@ -0,0 +1,193 @@
+<?php
+
+/**
+ * Functions for dealing with OpenID trust roots
+ */
+
+/**
+ * Parse a URL into its trust_root parts.
+ *
+ * @param string $trust_root: The url to parse
+ *
+ * @return mixed $parsed: Either an associative array of trust root
+ * parts or false if parsing failed.
+ */
+function Auth_OpenID___normalizeTrustRoot($trust_root)
+{
+ $parts = @parse_url($trust_root);
+ if ($parts === false) {
+ return false;
+ }
+ $required_parts = array('scheme', 'host');
+ $forbidden_parts = array('user', 'pass', 'fragment');
+ $keys = array_keys($parts);
+ if (array_intersect($keys, $required_parts) != $required_parts) {
+ return false;
+ }
+
+ if (array_intersect($keys, $forbidden_parts) != array()) {
+ return false;
+ }
+
+ $scheme = strtolower($parts['scheme']);
+ $allowed_schemes = array('http', 'https');
+ if (!in_array($scheme, $allowed_schemes)) {
+ return false;
+ }
+ $parts['scheme'] = $scheme;
+
+ $host = strtolower($parts['host']);
+ $hostparts = explode('*', $host);
+ switch (count($hostparts)) {
+ case 1:
+ $parts['wildcard'] = false;
+ break;
+ case 2:
+ if ($hostparts[0] ||
+ ($hostparts[1] && substr($hostparts[1], 0, 1) != '.')) {
+ return false;
+ }
+ $host = $hostparts[1];
+ $parts['wildcard'] = true;
+ break;
+ default:
+ return false;
+ }
+ if (strpos($host, ':') !== false) {
+ return false;
+ }
+ $parts['host'] = $host;
+
+ if (isset($parts['path'])) {
+ $path = strtolower($parts['path']);
+ if (substr($path, -1) != '/') {
+ $path .= '/';
+ }
+ } else {
+ $path = '/';
+ }
+ $parts['path'] = $path;
+ if (!isset($parts['port'])) {
+ $parts['port'] = false;
+ }
+ return $parts;
+}
+
+define('Auth_OpenID___TLDs',
+ '/\.(com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|ac|' .
+ 'ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|bf|bg|' .
+ 'bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|' .
+ 'cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|' .
+ 'fi|fj|fk|fm|fo|fr|ga|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|' .
+ 'gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|' .
+ 'ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|' .
+ 'ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|' .
+ 'nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|' .
+ 'ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|' .
+ 'so|sr|st|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tm|tn|to|tp|tr|tt|tv|tw|tz|' .
+ 'ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)$/');
+
+/**
+ * Is this trust root sane?
+ *
+ * A trust root is sane if it is syntactically valid and it has a
+ * reasonable domain name. Specifically, the domain name must be more
+ * than one level below a standard TLD or more than two levels below a
+ * two-letter tld.
+ *
+ * For example, '*.com' is not a sane trust root, but '*.foo.com' is.
+ * '*.co.uk' is not sane, but '*.bbc.co.uk' is.
+ *
+ * This check is not always correct, but it attempts to err on the
+ * side of marking sane trust roots insane instead of marking insane
+ * trust roots sane. For example, 'kink.fm' is marked as insane even
+ * though it "should" (for some meaning of should) be marked sane.
+ *
+ * This function should be used when creating OpenID servers to alert
+ * the users of the server when a consumer attempts to get the user to
+ * accept a suspicious trust root.
+ *
+ * @param string $tr: The trust root to check
+ *
+ * @return bool $sanity: Whether the trust root looks OK
+ */
+function Auth_OpenID_saneTrustRoot($tr)
+{
+ $parts = Auth_OpenID___normalizeTrustRoot($tr);
+ if ($parts === false) {
+ return false;
+ }
+ if ($parts['host'] == 'localhost') {
+ return true;
+ }
+ preg_match(Auth_OpenID___TLDs, $parts['host'], $matches);
+ if (!$matches) {
+ return false;
+ }
+ $tld = $matches[1];
+ $elements = explode('.', $parts['host']);
+ $n = count($elements);
+ if ($parts['wildcard']) {
+ $n -= 1;
+ }
+ if (strlen($tld) == 2) {
+ $n -= 1;
+ }
+ if ($n <= 1) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Does this URL match the given trust root?
+ *
+ * Return whether the URL falls under the given trust root. This does
+ * not check whether the trust root is sane. If the URL or trust root
+ * do not parse, this function will return false.
+ *
+ * @param string $trust_root: The trust root to match against
+ *
+ * @param string $url: The URL to check
+ *
+ * @return bool $matches: Whether the URL matches against the trust root
+ */
+function Auth_OpenID_matchTrustRoot($trust_root, $url)
+{
+ $trust_root_parsed = Auth_OpenID___normalizeTrustRoot($trust_root);
+ $url_parsed = Auth_OpenID___normalizeTrustRoot($url);
+ if (!$trust_root_parsed || !$url_parsed) {
+ return false;
+ }
+
+ // Check hosts matching
+ if ($url_parsed['wildcard']) {
+ return false;
+ }
+ if ($trust_root_parsed['wildcard']) {
+ $host_tail = $trust_root_parsed['host'];
+ $host = $url_parsed['host'];
+ if ($host_tail &&
+ substr($host, -(strlen($host_tail))) != $host_tail &&
+ substr($host_tail, 1) != $host) {
+ return false;
+ }
+ } else {
+ if ($trust_root_parsed['host'] != $url_parsed['host']) {
+ return false;
+ }
+ }
+
+ // Check path matching
+ $base_path = $trust_root_parsed['path'];
+ $path = $url_parsed['path'];
+ if (substr($path, 0, strlen($base_path)) != $base_path) {
+ return false;
+ }
+
+ // The port and scheme need to match exactly
+ return ($trust_root_parsed['scheme'] == $url_parsed['scheme'] &&
+ $url_parsed['port'] === $trust_root_parsed['port']);
+}
+
+?> \ No newline at end of file
diff --git a/Tests/Auth/OpenID/TrustRoot.php b/Tests/Auth/OpenID/TrustRoot.php
new file mode 100644
index 0000000..54694d0
--- /dev/null
+++ b/Tests/Auth/OpenID/TrustRoot.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * Tests for the TrustRoot module
+ */
+
+require_once "Auth/OpenID/TrustRoot.php";
+require_once "Tests/Auth/OpenID/Util.php";
+require_once "PHPUnit.php";
+
+class Tests_Auth_OpenID_TRParseCase extends PHPUnit_TestCase {
+ function Tests_Auth_OpenID_TRParseCase($desc, $case, $expected)
+ {
+ $this->setName($desc);
+ $this->case = $case;
+ $this->expected = $expected;
+ }
+
+ function runTest()
+ {
+ $is_sane = Auth_OpenID_saneTrustRoot($this->case);
+ $parsed = (bool)Auth_OpenID___normalizeTrustRoot($this->case);
+ switch ($this->expected) {
+ case 'sane':
+ $this->assertTrue($is_sane);
+ $this->assertTrue($parsed);
+ break;
+ case 'insane':
+ $this->assertTrue($parsed);
+ $this->assertFalse($is_sane);
+ break;
+ default:
+ $this->assertFalse($parsed);
+ $this->assertFalse($is_sane);
+ }
+ }
+}
+
+class Tests_Auth_OpenID_TRMatchCase extends PHPUnit_TestCase {
+ function Tests_Auth_OpenID_TRMatchCase($desc, $tr, $rt, $matches)
+ {
+ $this->setName($desc);
+ $this->tr = $tr;
+ $this->rt = $rt;
+ $this->matches = $matches;
+ }
+
+ function runTest()
+ {
+ $matches = Auth_OpenID_matchTrustRoot($this->tr, $this->rt);
+ if ($this->matches && !$matches) {
+ var_export(Auth_OpenID___normalizeTrustRoot($this->tr));
+ var_export(Auth_OpenID___normalizeTrustRoot($this->rt));
+ }
+
+ $this->assertEquals((bool)$this->matches, (bool)$matches);
+ }
+}
+
+function Tests_Auth_OpenID_parseHeadings($data, $c)
+{
+ $heading_pat = '/(^|\n)' . $c . '{40}\n([^\n]+)\n' . $c . '{40}\n()/';
+ $offset = 0;
+ $headings = array();
+ while (true) {
+ preg_match($heading_pat, $data, $matches, PREG_OFFSET_CAPTURE, $offset);
+ if (!$matches) {
+ break;
+ }
+ $start = $matches[0][1];
+ $heading = $matches[2][0];
+ $end = $matches[3][1];
+ $headings[] = array('heading' => $heading,
+ 'start' => $start,
+ 'end' => $end,
+ );
+ $offset = $end;
+ }
+ return $headings;
+}
+
+function Tests_Auth_OpenID_getSections($data)
+{
+ $headings = Tests_Auth_OpenID_parseHeadings($data, '-');
+ $sections = array();
+ $n = count($headings);
+ for ($i = 0; $i < $n; ) {
+ $secdata = $headings[$i];
+ list($numtests, $desc) = explode(': ', $secdata['heading']);
+ $start = $secdata['end'];
+ $i += 1;
+ if ($i < $n) {
+ $blob = substr($data, $start, $headings[$i]['start'] - $start);
+ } else {
+ $blob = substr($data, $start);
+ }
+ $lines = explode("\n", trim($blob));
+ if (count($lines) != $numtests) {
+ trigger_error('Parse failure: ' . var_export($secdata, true),
+ E_USER_ERROR);
+ }
+ $sections[] = array('desc' => $desc, 'lines' => $lines,);
+ }
+ return $sections;
+}
+
+function Tests_Auth_OpenID_trParseTests($head, $tests)
+{
+ $tests = array('fail' => $tests[0],
+ 'insane' => $tests[1],
+ 'sane' => $tests[2]);
+ $testobjs = array();
+ foreach ($tests as $expected => $testdata) {
+ $lines = $testdata['lines'];
+ foreach ($lines as $line) {
+ $desc = sprintf("%s - %s: %s", $head,
+ $testdata['desc'], var_export($line, true));
+ $testobjs[] = new Tests_Auth_OpenID_TRParseCase(
+ $desc, $line, $expected);
+ }
+ }
+ return $testobjs;
+}
+
+function Tests_Auth_OpenID_trMatchTests($head, $tests)
+{
+ $tests = array(true => $tests[0], false => $tests[1]);
+ $testobjs = array();
+ foreach ($tests as $expected => $testdata) {
+ $lines = $testdata['lines'];
+ foreach ($lines as $line) {
+ $pat = '/^([^ ]+) +([^ ]+)$/';
+ preg_match($pat, $line, $matches);
+ list($_, $tr, $rt) = $matches;
+ $desc = sprintf("%s - %s: %s %s", $head, $testdata['desc'],
+ var_export($tr, true), var_export($rt, true));
+ $testobjs[] = new Tests_Auth_OpenID_TRMatchCase(
+ $desc, $tr, $rt, $expected);
+ }
+ }
+ return $testobjs;
+}
+
+function Tests_Auth_OpenID_trustRootTests()
+{
+ $data = Tests_Auth_OpenID_readdata('trustroot.txt');
+ list($parsehead, $matchhead) = Tests_Auth_OpenID_parseHeadings($data, '=');
+ $pe = $parsehead['end'];
+ $parsedata = substr($data, $pe, $matchhead['start'] - $pe);
+ $parsetests = Tests_Auth_OpenID_getSections($parsedata);
+ $parsecases = Tests_Auth_OpenID_trParseTests($parsehead['heading'],
+ $parsetests);
+
+ $matchdata = substr($data, $matchhead['end']);
+ $matchtests = Tests_Auth_OpenID_getSections($matchdata);
+ $matchcases = Tests_Auth_OpenID_trMatchTests($matchhead['heading'],
+ $matchtests);
+
+ return array_merge($parsecases, $matchcases);
+}
+
+class Tests_Auth_OpenID_TrustRoot extends PHPUnit_TestSuite {
+ function Tests_Auth_OpenID_TrustRoot($name)
+ {
+ $this->setName($name);
+
+ foreach (Tests_Auth_OpenID_trustRootTests() as $test) {
+ $this->addTest($test);
+ }
+ }
+} \ No newline at end of file
diff --git a/Tests/Auth/OpenID/data/trustroot.txt b/Tests/Auth/OpenID/data/trustroot.txt
new file mode 100644
index 0000000..0db7f50
--- /dev/null
+++ b/Tests/Auth/OpenID/data/trustroot.txt
@@ -0,0 +1,115 @@
+========================================
+Trust root parsing checking
+========================================
+
+----------------------------------------
+14: Does not parse
+----------------------------------------
+baz.org
+*.foo.com
+http://*.schtuff.*/
+ftp://foo.com
+ftp://*.foo.com
+http://*.foo.com:80:90/
+foo.*.com
+http://foo.*.com
+http://www.*
+http://*foo.com/
+
+
+
+5
+
+----------------------------------------
+12: Insane
+----------------------------------------
+http://*/
+https://*/
+http://*.com
+http://*.com/
+https://*.com/
+http://*.com.au/
+http://*.co.uk/
+http://*.foo.notatld/
+https://*.foo.notatld/
+http://*.museum/
+https://*.museum/
+http://kink.fm/should/be/sane
+
+----------------------------------------
+14: Sane
+----------------------------------------
+http://*.schtuff.com/
+http://*.foo.schtuff.com/
+http://*.schtuff.com
+http://www.schtuff.com/
+http://www.schutff.com
+http://*.this.that.schtuff.com/
+http://*.foo.com/path
+http://*.foo.com/path?action=foo2
+http://x.foo.com/path?action=foo2
+http://x.foo.com/path?action=%3D
+http://localhost:8081/
+http://localhost:8082/?action=openid
+https://foo.com/
+http://goathack.livejournal.org:8020/openid/login.bml
+
+========================================
+return_to matching
+========================================
+
+----------------------------------------
+29: matches
+----------------------------------------
+http://*/ http://cnn.com/
+http://*/ http://livejournal.com/
+http://*/ http://met.museum/
+http://*:8081/ http://met.museum:8081/
+http://localhost:8081/x?action=openid http://localhost:8081/x?action=openid
+http://*.foo.com http://b.foo.com
+http://*.foo.com http://b.foo.com/
+http://*.foo.com/ http://b.foo.com
+http://b.foo.com http://b.foo.com
+http://b.foo.com http://b.foo.com/
+http://b.foo.com/ http://b.foo.com
+http://*.b.foo.com http://b.foo.com
+http://*.b.foo.com http://b.foo.com/
+http://*.b.foo.com/ http://b.foo.com
+http://*.b.foo.com http://x.b.foo.com
+http://*.b.foo.com http://w.x.b.foo.com
+http://*.bar.co.uk http://www.bar.co.uk
+http://*.uoregon.edu http://x.cs.uoregon.edu
+http://x.com/abc http://x.com/abc
+http://x.com/abc http://x.com/abc/def
+http://*.x.com http://x.com/gallery
+http://*.x.com http://foo.x.com/gallery
+http://foo.x.com http://foo.x.com/gallery/xxx
+http://*.x.com/gallery http://foo.x.com/gallery
+http://localhost:8082/?action=openid http://localhost:8082/?action=openid
+http://goathack.livejournal.org:8020/ http://goathack.livejournal.org:8020/openid/login.bml
+https://foo.com https://foo.com
+http://Foo.com http://foo.com
+http://foo.com http://Foo.com
+
+----------------------------------------
+19: does not match
+----------------------------------------
+http://*/ ftp://foo.com/
+http://*/ xxx
+http://*.x.com/abc http://foo.x.com
+http://*.x.com/abc http://*.x.com
+http://*.com/ http://*.com/
+http://x.com/abc http://x.com/
+http://x.com/abc http://x.com/a
+http://x.com/abc http://x.com/ab
+http://x.com/abc http://x.com/abcd
+http://*.cs.uoregon.edu http://x.uoregon.edu
+http://*.foo.com http://bar.com
+http://*.foo.com http://www.bar.com
+http://*.bar.co.uk http://xxx.co.uk
+https://foo.com http://foo.com
+http://foo.com https://foo.com
+http://foo.com:80 http://foo.com
+http://foo.com http://foo.com:80
+http://foo.com:81 http://foo.com:80
+http://*:80 http://foo.com:81
diff --git a/Tests/TestDriver.php b/Tests/TestDriver.php
index b8a1dc4..cd7faf8 100644
--- a/Tests/TestDriver.php
+++ b/Tests/TestDriver.php
@@ -74,7 +74,8 @@ $_test_names = array(
'OIDUtil',
'Parse',
'StoreTest',
- 'Server'
+ 'Server',
+ 'TrustRoot',
);
function selectTests($names)