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
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();
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;
throw error;
}
- verifySANs(error, profile, parseSANBox(SANsStr), ctx.getTarget());
+ verifySANs(error, profile, parseSANBox(SANsStr), ctx.getTarget(), ctx.getActor());
if ( !error.isEmpty()) {
throw error;
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");
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) {
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
--- /dev/null
+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;
+ }
+
+}
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;
+import org.cacert.gigi.util.CAA.CAARecord;
+
public class DNSUtil {
private static InitialDirContext context;
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])));
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])));
}
}
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.");
}
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])) {
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]*)*")) {
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);
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);
--- /dev/null
+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());
+ }
+
+}