From: Felix Dörre Date: Thu, 24 Dec 2015 11:15:17 +0000 (+0100) Subject: add: check CAA entries X-Git-Url: https://code.wpia.club/?p=gigi.git;a=commitdiff_plain;h=b05ce94112d4aa98c6fe6eba0ceddf973d3f09b2 add: check CAA entries fixes #70 Change-Id: I80b7a53a1b180c7f3b5462f8b07312a331ae6818 --- diff --git a/config/test.properties.template b/config/test.properties.template index 232f46e0..5a63cab9 100644 --- a/config/test.properties.template +++ b/config/test.properties.template @@ -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 diff --git a/src/org/cacert/gigi/dbObjects/Domain.java b/src/org/cacert/gigi/dbObjects/Domain.java index ba8aacff..eecf3766 100644 --- a/src/org/cacert/gigi/dbObjects/Domain.java +++ b/src/org/cacert/gigi/dbObjects/Domain.java @@ -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(); diff --git a/src/org/cacert/gigi/pages/account/certs/CertificateRequest.java b/src/org/cacert/gigi/pages/account/certs/CertificateRequest.java index aafd869c..ad915d7d 100644 --- a/src/org/cacert/gigi/pages/account/certs/CertificateRequest.java +++ b/src/org/cacert/gigi/pages/account/certs/CertificateRequest.java @@ -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 sANs2, CertificateOwner owner) { + private void verifySANs(GigiApiException error, CertificateProfile p, Set sANs2, CertificateOwner owner, User user) { Set 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 index 00000000..a95977e7 --- /dev/null +++ b/src/org/cacert/gigi/util/CAA.java @@ -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; + } + +} diff --git a/src/org/cacert/gigi/util/DNSUtil.java b/src/org/cacert/gigi/util/DNSUtil.java index 31fb1d6e..d0c772af 100644 --- a/src/org/cacert/gigi/util/DNSUtil.java +++ b/src/org/cacert/gigi/util/DNSUtil.java @@ -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 env = new Hashtable(); + 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]))); } } diff --git a/src/org/cacert/gigi/util/DomainAssessment.java b/src/org/cacert/gigi/util/DomainAssessment.java index 5070e5cb..f48012f8 100644 --- a/src/org/cacert/gigi/util/DomainAssessment.java +++ b/src/org/cacert/gigi/util/DomainAssessment.java @@ -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]*)*")) { diff --git a/tests/org/cacert/gigi/DomainVerification.java b/tests/org/cacert/gigi/DomainVerification.java index 1ce02be5..cc2d01ab 100644 --- a/tests/org/cacert/gigi/DomainVerification.java +++ b/tests/org/cacert/gigi/DomainVerification.java @@ -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); diff --git a/tests/org/cacert/gigi/testUtils/ManagedTest.java b/tests/org/cacert/gigi/testUtils/ManagedTest.java index 61689b55..17dcc5c9 100644 --- a/tests/org/cacert/gigi/testUtils/ManagedTest.java +++ b/tests/org/cacert/gigi/testUtils/ManagedTest.java @@ -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 index 00000000..3d42e3e7 --- /dev/null +++ b/tests/org/cacert/gigi/util/TestCAAValidation.java @@ -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 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.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()); + } + +}