add: check CAA entries
authorFelix Dörre <felix@dogcraft.de>
Thu, 24 Dec 2015 11:15:17 +0000 (12:15 +0100)
committerFelix Dörre <felix@dogcraft.de>
Sun, 10 Jul 2016 12:32:23 +0000 (14:32 +0200)
fixes #70

Change-Id: I80b7a53a1b180c7f3b5462f8b07312a331ae6818

config/test.properties.template
src/org/cacert/gigi/dbObjects/Domain.java
src/org/cacert/gigi/pages/account/certs/CertificateRequest.java
src/org/cacert/gigi/util/CAA.java [new file with mode: 0644]
src/org/cacert/gigi/util/DNSUtil.java
src/org/cacert/gigi/util/DomainAssessment.java
tests/org/cacert/gigi/DomainVerification.java
tests/org/cacert/gigi/testUtils/ManagedTest.java
tests/org/cacert/gigi/util/TestCAAValidation.java [new file with mode: 0644]

index 232f46e09afbf701eb8852db233106fd6793cd72..5a63cab98f54085f8042bd9be344b4121ca7ea62 100644 (file)
@@ -34,7 +34,7 @@ domain.testns=the.authorativ.ns.for.domain.dnstest
 domain.local=a.domain.that.resolves.to.localhost
 #port that is 80 is redirected to
 domain.localHTTP=80
-
+domain.CAAtest=-failing.domain +succeeding.domain
 
 email.address=somemail@yourdomain.org
 email.password=somemails-imap-password
index ba8aacff2dd1a7035a089d8c10dfe6866f512cc3..eecf37669a3e91438e40ba0c8bf103c7de87caca 100644 (file)
@@ -34,7 +34,7 @@ public class Domain implements IdCachable, Verifyable {
     public Domain(User actor, CertificateOwner owner, String suffix) throws GigiApiException {
         suffix = suffix.toLowerCase();
         synchronized (Domain.class) {
-            DomainAssessment.checkCertifiableDomain(suffix, actor.isInGroup(Group.CODESIGNING));
+            DomainAssessment.checkCertifiableDomain(suffix, actor.isInGroup(Group.CODESIGNING), true);
             this.owner = owner;
             this.suffix = suffix;
             insert();
index aafd869c9b6274ab8261f47988e43a1f1952a6a6..ad915d7da370f4b0163ed456a38bc4539a9431fb 100644 (file)
@@ -26,10 +26,13 @@ import org.cacert.gigi.dbObjects.CertificateOwner;
 import org.cacert.gigi.dbObjects.CertificateProfile;
 import org.cacert.gigi.dbObjects.CertificateProfile.PropertyTemplate;
 import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.Group;
 import org.cacert.gigi.dbObjects.Organisation;
 import org.cacert.gigi.dbObjects.User;
 import org.cacert.gigi.output.template.SprintfCommand;
 import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.CAA;
+import org.cacert.gigi.util.DomainAssessment;
 import org.cacert.gigi.util.PEM;
 import org.cacert.gigi.util.RateLimit;
 
@@ -314,7 +317,7 @@ public class CertificateRequest {
             throw error;
         }
 
-        verifySANs(error, profile, parseSANBox(SANsStr), ctx.getTarget());
+        verifySANs(error, profile, parseSANBox(SANsStr), ctx.getTarget(), ctx.getActor());
 
         if ( !error.isEmpty()) {
             throw error;
@@ -322,7 +325,7 @@ public class CertificateRequest {
         return true;
     }
 
-    private void verifySANs(GigiApiException error, CertificateProfile p, Set<SubjectAlternateName> sANs2, CertificateOwner owner) {
+    private void verifySANs(GigiApiException error, CertificateProfile p, Set<SubjectAlternateName> sANs2, CertificateOwner owner, User user) {
         Set<SubjectAlternateName> filteredSANs = new LinkedHashSet<>();
         PropertyTemplate domainTemp = p.getTemplates().get("domain");
         PropertyTemplate emailTemp = p.getTemplates().get("email");
@@ -331,7 +334,14 @@ public class CertificateRequest {
         for (SubjectAlternateName san : sANs2) {
             if (san.getType() == SANType.DNS) {
                 if (domainTemp != null && owner.isValidDomain(san.getName())) {
-                    if (pDNS != null && !domainTemp.isMultiple()) {
+                    boolean valid;
+                    try {
+                        DomainAssessment.checkCertifiableDomain(san.getName(), user.isInGroup(Group.CODESIGNING), false);
+                        valid = true;
+                    } catch (GigiApiException e) {
+                        valid = false;
+                    }
+                    if ( !valid || !CAA.verifyDomainAccess(owner, p, san.getName()) || (pDNS != null && !domainTemp.isMultiple())) {
                         // remove
                     } else {
                         if (pDNS == null) {
@@ -370,7 +380,7 @@ public class CertificateRequest {
         PropertyTemplate emailTemp = profile.getTemplates().get("email");
         PropertyTemplate nameTemp = profile.getTemplates().get("name");
         PropertyTemplate wotUserTemp = profile.getTemplates().get("name=WoTUser");
-        verifySANs(error, profile, SANs, ctx.getTarget());
+        verifySANs(error, profile, SANs, ctx.getTarget(), ctx.getActor());
 
         // Ok, let's determine the CN
         // the CN is
diff --git a/src/org/cacert/gigi/util/CAA.java b/src/org/cacert/gigi/util/CAA.java
new file mode 100644 (file)
index 0000000..a95977e
--- /dev/null
@@ -0,0 +1,117 @@
+package org.cacert.gigi.util;
+
+import javax.naming.NamingException;
+
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+
+public class CAA {
+
+    public static class CAARecord {
+
+        private byte flags;
+
+        private String tag;
+
+        private String data;
+
+        public CAARecord(byte[] rec) {
+            byte length = (byte) (rec[1] & 0xFF);
+            tag = new String(rec, 2, length);
+            data = new String(rec, 2 + length, rec.length - 2 - length);
+            flags = rec[0];
+        }
+
+        @Override
+        public String toString() {
+            return "CAA " + (flags & 0xFF) + " " + tag + " " + data;
+        }
+
+        public String getData() {
+            return data;
+        }
+
+        public byte getFlags() {
+            return flags;
+        }
+
+        public String getTag() {
+            return tag;
+        }
+
+        public boolean isCritical() {
+            return (flags & (byte) 0x80) == (byte) 0x80;
+        }
+    }
+
+    public static boolean verifyDomainAccess(CertificateOwner owner, CertificateProfile p, String name) {
+        try {
+            if (name.startsWith("*.")) {
+                return verifyDomainAccess(owner, p, name.substring(2), true);
+            }
+            return verifyDomainAccess(owner, p, name, false);
+        } catch (NamingException e) {
+            return false;
+        }
+    }
+
+    private static boolean verifyDomainAccess(CertificateOwner owner, CertificateProfile p, String name, boolean wild) throws NamingException {
+        CAARecord[] caa = getEffectiveCAARecords(name);
+        if (caa.length == 0) {
+            return true; // default assessment is beeing granted
+        }
+        for (int i = 0; i < caa.length; i++) {
+            CAARecord r = caa[i];
+            if (r.getTag().equals("issuewild")) {
+                if (wild && authorized(owner, p, r.getData())) {
+                    return true;
+                }
+            } else if (r.getTag().equals("iodef")) {
+                // TODO send mail/form
+            } else if (r.getTag().equals("issue")) {
+                if ( !wild && authorized(owner, p, r.getData())) {
+                    return true;
+                }
+            } else {
+                if (r.isCritical()) {
+                    return false; // found critical, unkown entry
+                }
+                // ignore unkown tags
+            }
+        }
+        return false;
+    }
+
+    private static CAARecord[] getEffectiveCAARecords(String name) throws NamingException {
+        CAARecord[] caa = DNSUtil.getCAAEntries(name);
+        // TODO missing alias processing
+        while (caa.length == 0 && name.contains(".")) {
+            name = name.split("\\.", 2)[1];
+            caa = DNSUtil.getCAAEntries(name);
+        }
+        return caa;
+    }
+
+    private static boolean authorized(CertificateOwner owner, CertificateProfile p, String data) {
+        String[] parts = data.split(";");
+        String ca = parts[0].trim();
+        if ( !ca.equals("cacert.org")) {
+            return false;
+        }
+        for (int i = 1; i < parts.length; i++) {
+            String[] pa = parts[i].split("=");
+            String key = pa[0].trim();
+            String v = pa[1].trim();
+            if (key.equals("account")) {
+                int id = Integer.parseInt(v);
+                if (id != owner.getId()) {
+                    return false;
+                }
+            } else { // unknown key... be conservative
+                return false;
+            }
+        }
+        return true;
+    }
+
+}
index 31fb1d6e2e88e00cc855e554cf520f748142cd4f..d0c772aff40861bacfced40503d55f21d58f0531 100644 (file)
@@ -9,6 +9,8 @@ import javax.naming.directory.Attribute;
 import javax.naming.directory.Attributes;
 import javax.naming.directory.InitialDirContext;
 
+import org.cacert.gigi.util.CAA.CAARecord;
+
 public class DNSUtil {
 
     private static InitialDirContext context;
@@ -66,6 +68,27 @@ public class DNSUtil {
         return extractTextEntries(dnsLookup.get("MX"));
     }
 
+    public static CAARecord[] getCAAEntries(String domain) throws NamingException {
+        Hashtable<String, String> env = new Hashtable<String, String>();
+        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
+        InitialDirContext context = new InitialDirContext(env);
+
+        Attributes dnsLookup = context.getAttributes(domain, new String[] {
+                "257"
+        });
+        Attribute nsRecords = dnsLookup.get("257");
+        if (nsRecords == null) {
+            return new CAARecord[] {};
+        }
+        CAA.CAARecord[] result = new CAA.CAARecord[nsRecords.size()];
+        for (int i = 0; i < result.length; i++) {
+            byte[] rec = (byte[]) nsRecords.get(i);
+
+            result[i] = new CAA.CAARecord(rec);
+        }
+        return result;
+    }
+
     public static void main(String[] args) throws NamingException {
         if (args[0].equals("MX")) {
             System.out.println(Arrays.toString(getMXEntries(args[1])));
@@ -73,6 +96,8 @@ public class DNSUtil {
             System.out.println(Arrays.toString(getNSNames(args[1])));
         } else if (args[0].equals("TXT")) {
             System.out.println(Arrays.toString(getTXTEntries(args[1], args[2])));
+        } else if (args[0].equals("CAA")) {
+            System.out.println(Arrays.toString(getCAAEntries(args[1])));
         }
     }
 
index 5070e5cb3e669d73d5011670eaf1a3ccfd1d7e72..f48012f8bc43223915c3341a2b4aa2e309f25b18 100644 (file)
@@ -72,7 +72,7 @@ public class DomainAssessment {
         return financial.contains(suffix);
     }
 
-    public static void checkCertifiableDomain(String domain, boolean hasPunycodeRight) throws GigiApiException {
+    public static void checkCertifiableDomain(String domain, boolean hasPunycodeRight, boolean asRegister) throws GigiApiException {
         if (isHighFinancialValue(domain)) {
             throw new GigiApiException("Domain blocked for automatic adding.");
         }
@@ -80,6 +80,7 @@ public class DomainAssessment {
         if (parts.length < 2) {
             throw new GigiApiException("Domain does not contain '.'.");
         }
+
         boolean neededPunycode = false;
         for (int i = parts.length - 1; i >= 0; i--) {
             if ( !isValidDomainPart(parts[i])) {
@@ -99,9 +100,11 @@ public class DomainAssessment {
             throw new GigiApiException("Punycode not allowed under this TLD.");
         }
 
-        String publicSuffix = PublicSuffixes.getInstance().getRegistrablePart(domain);
-        if ( !domain.equals(publicSuffix)) {
-            throw new GigiApiException("You may only register a domain with exactly one label before the public suffix.");
+        if (asRegister) {
+            String publicSuffix = PublicSuffixes.getInstance().getRegistrablePart(domain);
+            if ( !domain.equals(publicSuffix)) {
+                throw new GigiApiException("You may only register a domain with exactly one label before the public suffix.");
+            }
         }
 
         if (("." + domain).matches("(\\.[0-9]*)*")) {
index 1ce02be5151bb81ce7df6db3e2fdb13328e7fc88..cc2d01ab4146e5ac2364fd22e49efabdb311618b 100644 (file)
@@ -77,7 +77,7 @@ public class DomainVerification extends ConfiguredTest {
 
     private void isCertifiableDomain(boolean b, String string, boolean puny) {
         try {
-            DomainAssessment.checkCertifiableDomain(string, puny);
+            DomainAssessment.checkCertifiableDomain(string, puny, true);
             assertTrue(b);
         } catch (GigiApiException e) {
             assertFalse(e.getMessage(), b);
index 61689b55f8835235de2e62944ed16ddcfe9a0906..17dcc5c9621e616a429b4a6175aaf0d9259e0bef 100644 (file)
@@ -101,10 +101,15 @@ public class ManagedTest extends ConfiguredTest {
         initEnvironment();
     }
 
+    private static boolean inited = false;
+
     public static Properties initEnvironment() {
         try {
             Properties mainProps = ConfiguredTest.initEnvironment();
-
+            if (inited) {
+                return mainProps;
+            }
+            inited = true;
             purgeDatabase();
             String type = testProps.getProperty("type");
             generateMainProps(mainProps);
diff --git a/tests/org/cacert/gigi/util/TestCAAValidation.java b/tests/org/cacert/gigi/util/TestCAAValidation.java
new file mode 100644 (file)
index 0000000..3d42e3e
--- /dev/null
@@ -0,0 +1,81 @@
+package org.cacert.gigi.util;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.Job;
+import org.cacert.gigi.pages.account.certs.CertificateRequest;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestCAAValidation extends ClientTest {
+
+    @Parameters(name = "CAATest({0}) = {1}")
+    public static Iterable<Object[]> genParams() throws IOException {
+        initEnvironment();
+
+        String caa = (String) getTestProps().get("domain.CAAtest");
+        assumeNotNull(caa);
+        String[] parts = caa.split(" ");
+        Object[][] res = new Object[parts.length][];
+        for (int i = 0; i < res.length; i++) {
+            char firstChar = parts[i].charAt(0);
+            if (firstChar != '-' && firstChar != '+') {
+                throw new Error("malformed CAA test vector");
+            }
+            res[i] = new Object[] {
+                    parts[i].substring(1), firstChar == '+'
+            };
+        }
+        return Arrays.<Object[]>asList(res);
+    }
+
+    @Parameter(0)
+    public String domain;
+
+    @Parameter(1)
+    public Boolean success;
+
+    @Test
+    public void testCAA() {
+        assertEquals(success, CAA.verifyDomainAccess(u, CertificateProfile.getByName("server"), domain));
+    }
+
+    @Test
+    public void testCAACert() throws GeneralSecurityException, IOException, GigiApiException, InterruptedException {
+        Domain d = new Domain(u, u, domain);
+        verify(d);
+        String csr = generatePEMCSR(generateKeypair(), "CN=test");
+        CertificateRequest cr = new CertificateRequest(new AuthorizationContext(u, u), csr);
+        try {
+            cr.update("", Digest.SHA512.toString(), "server", null, null, "dns:" + domain + "\n", null, null);
+        } catch (GigiApiException e) {
+            assertThat(e.getMessage(), containsString("has been removed"));
+            assertFalse(success);
+            return;
+        }
+        assertTrue(success);
+        Certificate draft = cr.draft();
+        Job j = draft.issue(null, "2y", u);
+        await(j);
+
+        assertEquals(CertificateStatus.ISSUED, draft.getStatus());
+    }
+
+}