From 12c6327bdc31d1f1d50159de69641d878827dddf Mon Sep 17 00:00:00 2001 From: =?utf8?q?Felix=20D=C3=B6rre?= Date: Sun, 9 Nov 2014 00:02:29 +0100 Subject: [PATCH] ADD: more advanced domain name verification (public suffix, punycode) --- doc/jenkinsJob/config.xml | 4 +- src/org/cacert/gigi/dbObjects/CPS.properties | 3 + src/org/cacert/gigi/dbObjects/Domain.java | 76 ++++++++++++++++++- tests/org/cacert/gigi/DomainVerification.java | 76 +++++++++++++++++++ tests/org/cacert/gigi/TestDomain.java | 70 +++-------------- tests/org/cacert/gigi/TestUser.java | 20 ++--- .../cacert/gigi/pages/account/TestDomain.java | 4 +- 7 files changed, 178 insertions(+), 75 deletions(-) create mode 100644 src/org/cacert/gigi/dbObjects/CPS.properties create mode 100644 tests/org/cacert/gigi/DomainVerification.java diff --git a/doc/jenkinsJob/config.xml b/doc/jenkinsJob/config.xml index 65ee7bd2..505a7f13 100644 --- a/doc/jenkinsJob/config.xml +++ b/doc/jenkinsJob/config.xml @@ -77,7 +77,7 @@ KEYSIZE=4096 EOT cat <<EOT >config/test.properties type=autonomous -java=/usr/lib/jvm/openjdk-8-jdk-gigi/bin/java -cp gigi-testing.jar:/usr/share/java/mysql-connector-java.jar -javaagent:/usr/share/java/jacocoagent.jar org.cacert.gigi.Launcher +java=/usr/lib/jvm/openjdk-8-jdk-gigi/bin/java -cp bintest:gigi-testing.jar:/usr/share/java/mysql-connector-java.jar -javaagent:/usr/share/java/jacocoagent.jar org.cacert.gigi.Launcher serverPort.https=4448 serverPort.http=8098 mailPort=8473 @@ -111,7 +111,7 @@ EOT -Dfile.encoding=UTF-8 cacert-gigi/build.xml juintexec=$$$$JUNIT_PATH$$$$ -test_nic=$$$$YOUR_TESTSERVICE_NIC$$$$ +test_nic=$$$$YOUR_TESTSERVICE_NIC$$$$\n$$$$YOUR_LOOKUP_DOMAIN$$$$ cd cacert-gigi diff --git a/src/org/cacert/gigi/dbObjects/CPS.properties b/src/org/cacert/gigi/dbObjects/CPS.properties new file mode 100644 index 00000000..87acaed7 --- /dev/null +++ b/src/org/cacert/gigi/dbObjects/CPS.properties @@ -0,0 +1,3 @@ +# from http://www.cacert.org/policy/CertificationPracticeStatement.php#p3.1.7 on 07.11.2014 +IDN-enabled=ac,ar,at,biz,br,cat,ch,cl,cn,de,dk,es,fi,gr,hu,info,io,ir,is,jp,kr,li,lt,museum,no,org,pl,pr,se,sh,th,tm,tw,vn +# from https://data.iana.org/TLD/tlds-alpha-by-domain.txt diff --git a/src/org/cacert/gigi/dbObjects/Domain.java b/src/org/cacert/gigi/dbObjects/Domain.java index 13c8436c..13ee9f35 100644 --- a/src/org/cacert/gigi/dbObjects/Domain.java +++ b/src/org/cacert/gigi/dbObjects/Domain.java @@ -1,14 +1,21 @@ package org.cacert.gigi.dbObjects; +import java.io.IOException; +import java.net.IDN; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Properties; +import java.util.Set; import org.cacert.gigi.GigiApiException; import org.cacert.gigi.database.DatabaseConnection; import org.cacert.gigi.database.GigiPreparedStatement; import org.cacert.gigi.database.GigiResultSet; import org.cacert.gigi.dbObjects.DomainPingConfiguration.PingType; +import org.cacert.gigi.util.PublicSuffixes; public class Domain implements IdCachable { @@ -60,6 +67,17 @@ public class Domain implements IdCachable { private int id; + private static final Set IDNEnabledTLDs; + static { + Properties CPS = new Properties(); + try { + CPS.load(Domain.class.getResourceAsStream("CPS.properties")); + IDNEnabledTLDs = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(CPS.getProperty("IDN-enabled").split(",")))); + } catch (IOException e) { + throw new Error(e); + } + } + private Domain(int id) { GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT memid, domain FROM `domains` WHERE id=? AND deleted IS NULL"); ps.setInt(1, id); @@ -75,16 +93,72 @@ public class Domain implements IdCachable { } public Domain(User owner, String suffix) throws GigiApiException { + checkCertifyableDomain(suffix, owner.isInGroup(Group.getByString("codesign"))); this.owner = owner; this.suffix = suffix; } + public static void checkCertifyableDomain(String s, boolean hasPunycodeRight) throws GigiApiException { + String[] parts = s.split("\\.", -1); + if (parts.length < 2) { + throw new GigiApiException("Domain does not contain '.'."); + } + for (int i = parts.length - 1; i >= 0; i--) { + if ( !isVaildDomainPart(parts[i], hasPunycodeRight)) { + throw new GigiApiException("Syntax error in Domain"); + } + } + String publicSuffix = PublicSuffixes.getInstance().getRegistrablePart(s); + if ( !s.equals(publicSuffix)) { + throw new GigiApiException("You may only register a domain with exactly one lable before the public suffix."); + } + checkPunycode(parts[0], s.substring(parts[0].length() + 1)); + } + + private static void checkPunycode(String label, String domainContext) throws GigiApiException { + if (label.charAt(2) != '-' || label.charAt(3) != '-') { + return; // is no punycode + } + if ( !IDNEnabledTLDs.contains(domainContext)) { + throw new GigiApiException("Punycode label could not be positively verified."); + } + if ( !label.startsWith("xn--")) { + throw new GigiApiException("Unknown ACE prefix."); + } + try { + String unicode = IDN.toUnicode(label); + if (unicode.startsWith("xn--")) { + throw new GigiApiException("Punycode label could not be positively verified."); + } + } catch (IllegalArgumentException e) { + throw new GigiApiException("Punycode label could not be positively verified."); + } + } + + public static boolean isVaildDomainPart(String s, boolean allowPunycode) { + if ( !s.matches("[a-z0-9-]+")) { + return false; + } + if (s.charAt(0) == '-' || s.charAt(s.length() - 1) == '-') { + return false; + } + if (s.length() > 63) { + return false; + } + boolean canBePunycode = s.length() >= 4 && s.charAt(2) == '-' && s.charAt(3) == '-'; + if (canBePunycode && !allowPunycode) { + return false; + } + return true; + } + private static void checkInsert(String suffix) throws GigiApiException { - GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT 1 FROM `domains` WHERE (domain=RIGHT(?,LENGTH(domain)) OR RIGHT(domain,LENGTH(?))=?) AND deleted IS NULL"); + GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT 1 FROM `domains` WHERE (domain=? OR (CONCAT('.', domain)=RIGHT(?,LENGTH(domain)+1) OR RIGHT(domain,LENGTH(?)+1)=CONCAT('.',?))) AND deleted IS NULL"); ps.setString(1, suffix); ps.setString(2, suffix); ps.setString(3, suffix); + ps.setString(4, suffix); GigiResultSet rs = ps.executeQuery(); boolean existed = rs.next(); rs.close(); diff --git a/tests/org/cacert/gigi/DomainVerification.java b/tests/org/cacert/gigi/DomainVerification.java new file mode 100644 index 00000000..bc6f5fdb --- /dev/null +++ b/tests/org/cacert/gigi/DomainVerification.java @@ -0,0 +1,76 @@ +package org.cacert.gigi; + +import static org.junit.Assert.*; + +import org.cacert.gigi.dbObjects.Domain; +import org.junit.Test; + +public class DomainVerification { + + @Test + public void testDomainPart() { + assertTrue(Domain.isVaildDomainPart("cacert", false)); + assertTrue(Domain.isVaildDomainPart("de", false)); + assertTrue(Domain.isVaildDomainPart("ha2-a", false)); + assertTrue(Domain.isVaildDomainPart("ha2--a", false)); + assertTrue(Domain.isVaildDomainPart("h--a", false)); + assertFalse(Domain.isVaildDomainPart("xn--bla", false)); + assertFalse(Domain.isVaildDomainPart("-xnbla", false)); + assertFalse(Domain.isVaildDomainPart("xnbla-", false)); + assertFalse(Domain.isVaildDomainPart("", false)); + assertTrue(Domain.isVaildDomainPart("2xnbla", false)); + assertTrue(Domain.isVaildDomainPart("xnbla2", false)); + assertTrue(Domain.isVaildDomainPart("123", false)); + assertTrue(Domain.isVaildDomainPart("abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy1234567890123", false)); + assertFalse(Domain.isVaildDomainPart("abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy12345678901234", false)); + } + + @Test + public void testDomainCertifyable() { + isCertifyableDomain(true, "cacert.org", false); + isCertifyableDomain(true, "cacert.de", false); + isCertifyableDomain(true, "cacert.org", false); + isCertifyableDomain(true, "cacert.org", false); + isCertifyableDomain(true, "1234.org", false); + isCertifyableDomain(false, "a.cacert.org", true); + isCertifyableDomain(false, "gigi.local", true); + isCertifyableDomain(false, "org", true); + isCertifyableDomain(false, "'a.org", true); + isCertifyableDomain(false, ".org", true); + isCertifyableDomain(false, ".org.", true); + // non-real-punycode + isCertifyableDomain(true, "xna-ae.de", false); + isCertifyableDomain(true, "xn-aae.de", false); + + // illegal punycode: + // illegal ace prefix + isCertifyableDomain(false, "aa--b.com", true); + isCertifyableDomain(false, "xm--ae-a.de", true); + + // illegal punycode content + isCertifyableDomain(false, "xn--ae-a.com", true); + isCertifyableDomain(false, "xn--ae.de", true); + isCertifyableDomain(false, "xn--ae-a.org", true); + isCertifyableDomain(false, "xn--ae-a.de", true); + // valid punycode requires permission + isCertifyableDomain(true, "xn--4ca0bs.de", true); + isCertifyableDomain(false, "xn--4ca0bs.de", false); + isCertifyableDomain(true, "xn--a-zfa9cya.de", true); + isCertifyableDomain(false, "xn--a-zfa9cya.de", false); + + // valid punycode does not help under .com + isCertifyableDomain(false, "xn--a-zfa9cya.com", true); + isCertifyableDomain(true, "zfa9cya.com", true); + + } + + private void isCertifyableDomain(boolean b, String string, boolean puny) { + try { + Domain.checkCertifyableDomain(string, puny); + assertTrue(b); + } catch (GigiApiException e) { + assertFalse(e.getMessage(), b); + } + } + +} diff --git a/tests/org/cacert/gigi/TestDomain.java b/tests/org/cacert/gigi/TestDomain.java index 7ff9b53b..bf7118df 100644 --- a/tests/org/cacert/gigi/TestDomain.java +++ b/tests/org/cacert/gigi/TestDomain.java @@ -19,29 +19,29 @@ public class TestDomain extends ManagedTest { @Test public void testDomain() throws InterruptedException, GigiApiException { assertEquals(0, us.getDomains().length); - Domain d = new Domain(us, "v1.example.org"); + Domain d = new Domain(us, "v1example.org"); assertEquals(0, d.getId()); d.insert(); Domain[] domains = us.getDomains(); assertEquals(1, domains.length); - assertEquals("v1.example.org", domains[0].getSuffix()); + assertEquals("v1example.org", domains[0].getSuffix()); assertEquals(domains[0].getOwner().getId(), us.getId()); assertNotEquals(0, domains[0].getId()); assertNotEquals(0, d.getId()); assertEquals(d.getId(), domains[0].getId()); - Domain d2 = new Domain(us, "v2.example.org"); + Domain d2 = new Domain(us, "v2-example.org"); assertEquals(0, d2.getId()); d2.insert(); domains = us.getDomains(); assertEquals(2, domains.length); - if ( !domains[1].getSuffix().equals("v2.example.org")) { + if ( !domains[1].getSuffix().equals("v2-example.org")) { Domain d1 = domains[0]; domains[0] = domains[1]; domains[1] = d1; } - assertEquals("v2.example.org", domains[1].getSuffix()); + assertEquals("v2-example.org", domains[1].getSuffix()); assertEquals(domains[0].getOwner().getId(), us.getId()); assertEquals(domains[1].getOwner().getId(), us.getId()); assertNotEquals(0, domains[0].getId()); @@ -52,10 +52,10 @@ public class TestDomain extends ManagedTest { @Test public void testDoubleDomain() throws InterruptedException, GigiApiException { - Domain d = new Domain(us, "dub.example.org"); + Domain d = new Domain(us, "dub-example.org"); d.insert(); try { - Domain d2 = new Domain(us, "dub.example.org"); + Domain d2 = new Domain(us, "dub-example.org"); d2.insert(); fail("expected exception"); } catch (GigiApiException e) { @@ -65,66 +65,16 @@ public class TestDomain extends ManagedTest { @Test public void testDoubleDomainDelete() throws InterruptedException, GigiApiException { - Domain d = new Domain(us, "del.example.org"); + Domain d = new Domain(us, "delexample.org"); d.insert(); d.delete(); - Domain d2 = new Domain(us, "del.example.org"); + Domain d2 = new Domain(us, "delexample.org"); d2.insert(); } - @Test - public void testPrefixCheck() throws InterruptedException, GigiApiException { - String uni = createUniqueName() + "un.tld"; - Domain d0 = new Domain(us, uni); - d0.insert(); - d0.delete(); - Domain d = new Domain(us, "pref." + uni); - d.insert(); - - Domain d2 = new Domain(us, uni); - try { - d2.insert(); - fail("Prefix match failed"); - } catch (GigiApiException e) { - } - d2 = new Domain(us, "a.pref." + uni); - try { - d2.insert(); - fail("Prefix match failed"); - } catch (GigiApiException e) { - } - d2 = new Domain(us, "pref." + uni); - try { - d2.insert(); - fail("exact match failed"); - } catch (GigiApiException e) { - } - - } - - @Test - public void testDoubleDomainPrefix() throws InterruptedException, GigiApiException { - Domain d = new Domain(us, "pref.aexample.org"); - d.insert(); - Domain d2 = new Domain(us, "a.pref.aexample.org"); - try { - d2.insert(); - fail("expected exception"); - } catch (GigiApiException e) { - // expected - } - Domain d3 = new Domain(us, "aexample.org"); - try { - d3.insert(); - fail("expected exception"); - } catch (GigiApiException e) { - // expected - } - } - @Test public void testDoubleInsertDomain() throws InterruptedException, GigiApiException { - Domain d = new Domain(us, "dins.example.org"); + Domain d = new Domain(us, "dins-example.org"); d.insert(); try { d.insert(); diff --git a/tests/org/cacert/gigi/TestUser.java b/tests/org/cacert/gigi/TestUser.java index 4a51af94..dffbbded 100644 --- a/tests/org/cacert/gigi/TestUser.java +++ b/tests/org/cacert/gigi/TestUser.java @@ -70,18 +70,18 @@ public class TestUser extends ManagedTest { User u = User.getById(id); new EmailAddress(u, uq + "b@email.org").insert(Language.getInstance(Locale.ENGLISH)); new EmailAddress(u, uq + "c@email.org").insert(Language.getInstance(Locale.ENGLISH)); - new Domain(u, uq + "a.testdomain.org").insert(); - new Domain(u, uq + "b.testdomain.org").insert(); - new Domain(u, uq + "c.testdomain.org").insert(); + new Domain(u, uq + "a-testdomain.org").insert(); + new Domain(u, uq + "b-testdomain.org").insert(); + new Domain(u, uq + "c-testdomain.org").insert(); assertEquals(3, u.getEmails().length); assertEquals(3, u.getDomains().length); - assertTrue(u.isValidDomain(uq + "a.testdomain.org")); - assertTrue(u.isValidDomain(uq + "b.testdomain.org")); - assertTrue(u.isValidDomain(uq + "c.testdomain.org")); - assertTrue(u.isValidDomain("a." + uq + "a.testdomain.org")); - assertTrue(u.isValidDomain("*." + uq + "a.testdomain.org")); - assertFalse(u.isValidDomain("a" + uq + "a.testdomain.org")); - assertFalse(u.isValidDomain("b" + uq + "a.testdomain.org")); + assertTrue(u.isValidDomain(uq + "a-testdomain.org")); + assertTrue(u.isValidDomain(uq + "b-testdomain.org")); + assertTrue(u.isValidDomain(uq + "c-testdomain.org")); + assertTrue(u.isValidDomain("a." + uq + "a-testdomain.org")); + assertTrue(u.isValidDomain("*." + uq + "a-testdomain.org")); + assertFalse(u.isValidDomain("a" + uq + "a-testdomain.org")); + assertFalse(u.isValidDomain("b" + uq + "a-testdomain.org")); assertTrue(u.isValidEmail(uq + "a@email.org")); assertTrue(u.isValidEmail(uq + "b@email.org")); diff --git a/tests/org/cacert/gigi/pages/account/TestDomain.java b/tests/org/cacert/gigi/pages/account/TestDomain.java index 0b82a56b..faa9ac8b 100644 --- a/tests/org/cacert/gigi/pages/account/TestDomain.java +++ b/tests/org/cacert/gigi/pages/account/TestDomain.java @@ -20,8 +20,8 @@ public class TestDomain extends ManagedTest { @Test public void testAdd() throws IOException { - assertNull(addDomain(session, uniq + ".tld")); - assertNotNull(addDomain(session, uniq + ".tld")); + assertNull(addDomain(session, uniq + ".de")); + assertNotNull(addDomain(session, uniq + ".de")); } public static String addDomain(String session, String domain) throws IOException { -- 2.39.2