From 2f0f5330fc8380b5b641fc2707b2b719bd4ed5c9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 25 Jul 2017 14:59:40 -0400 Subject: [PATCH] Add CAA support (#161) * Added CAA support * Fixed bind parsing of CAA records * Added CAA parsing test * Renamed CAA json fields * Added CAA tag validation * Updated CAA docs to clarify on the value field * parse_tests: Fixed typo in caaflags * Added integration test * Small cleanups --- docs/_functions/domain/CAA.md | 36 +++++++++ integrationTest/integration_test.go | 16 ++++ integrationTest/zones/example.com.zone | 2 +- models/dns.go | 12 ++- models/dns_test.go | 15 ++++ pkg/js/helpers.js | 19 +++++ pkg/js/parse_tests/014-caa.js | 13 +++ pkg/js/parse_tests/014-caa.json | 43 ++++++++++ pkg/js/static.go | 107 +++++++++++++------------ pkg/normalize/validate.go | 10 ++- pkg/normalize/validate_test.go | 18 +++++ providers/bind/bindProvider.go | 6 +- providers/bind/prettyzone.go | 13 +++ providers/bind/prettyzone_test.go | 27 +++++++ providers/google/google.go | 2 +- providers/providers.go | 2 + 16 files changed, 283 insertions(+), 58 deletions(-) create mode 100644 docs/_functions/domain/CAA.md create mode 100644 pkg/js/parse_tests/014-caa.js create mode 100644 pkg/js/parse_tests/014-caa.json diff --git a/docs/_functions/domain/CAA.md b/docs/_functions/domain/CAA.md new file mode 100644 index 000000000..38c5356d2 --- /dev/null +++ b/docs/_functions/domain/CAA.md @@ -0,0 +1,36 @@ +--- +name: CAA +parameters: + - name + - tag + - value + - modifiers... +--- + +CAA adds a CAA record to a domain. The name should be the relative label for the record. Use `@` for the domain apex. + +Tag can be one of "issue", "issuewild" or "iodef". + +Value is a string. The format of the contents is different depending on the tag. DNSControl will handle any escaping or quoting required, similer to TXT records. For example use `CAA("@", "issue", "letsencrypt.org")` rather than `CAA("@", "issue", "\"letsencrypt.org\"")`. + +Flags are controlled by modifier.: + +- CAA_CRITICAL: Issuer critical flag. CA that does not understand this tag will refuse to issue certificate for this domain. + +CAA record is supported only by BIND and Google Cloud DNS. Some certificate authorities may not support this record until the mandatory date of September 2017. + +{% include startExample.html %} +{% highlight js %} + +D("example.com", REGISTRAR, DnsProvider("GCLOUD"), + // Allow letsencrypt to issue certificate for this domain + CAA("@", "issue", "letsencrypt.org"), + // Allow no CA to issue wildcard certificate for this domain + CAA("@", "issuewild", ";"), + // Report all violation to test@example.com. If CA does not support + // this record then refuse to issue any certificate + CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL) +); + +{%endhighlight%} +{% include endExample.html %} diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 04f8d2ade..f27f03bc4 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -236,6 +236,13 @@ func srv(name string, priority, weight, port uint16, target string) *rec { return r } +func caa(name string, tag string, flag uint8, target string) *rec { + r := makeRec(name, target, "CAA") + r.CaaFlag = flag + r.CaaTag = tag + return r +} + func makeRec(name, target, typ string) *rec { return &rec{ Name: name, @@ -327,6 +334,15 @@ var tests = []*TestCase{ tc("Change Weight", srv("@", 52, 62, 7, "foo.com."), srv("@", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV), tc("Change Port", srv("@", 52, 62, 72, "foo.com."), srv("@", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV), + //CAA + tc("Empty").IfHasCapability(providers.CanUseCAA), + tc("CAA record", caa("@", "issue", 0, "letsencrypt.org")).IfHasCapability(providers.CanUseCAA), + tc("CAA change tag", caa("@", "issuewild", 0, "letsencrypt.org")).IfHasCapability(providers.CanUseCAA), + tc("CAA change target", caa("@", "issuewild", 0, "example.com")).IfHasCapability(providers.CanUseCAA), + tc("CAA change flag", caa("@", "issuewild", 1, "example.com")).IfHasCapability(providers.CanUseCAA), + tc("CAA many records", caa("@", "issue", 0, "letsencrypt.org"), caa("@", "issuewild", 0, ";"), caa("@", "iodef", 1, "mailto:test@example.com")).IfHasCapability(providers.CanUseCAA), + tc("CAA delete", caa("@", "issue", 0, "letsencrypt.org")).IfHasCapability(providers.CanUseCAA), + //TODO: in validation, check that everything is given in unicode. This case hurts too much. //tc("IDN pre-punycoded", cname("xn--o-0gab", "xn--o-0gab.xn--o-0gab.")), } diff --git a/integrationTest/zones/example.com.zone b/integrationTest/zones/example.com.zone index f57f16f4a..c564c8109 100644 --- a/integrationTest/zones/example.com.zone +++ b/integrationTest/zones/example.com.zone @@ -1,4 +1,4 @@ $TTL 300 -@ IN SOA DEFAULT_NOT_SET. DEFAULT_NOT_SET. 2017071990 3600 600 604800 1440 +@ IN SOA DEFAULT_NOT_SET. DEFAULT_NOT_SET. 2017072603 3600 600 604800 1440 IN NS ns1.otherdomain.tld. IN NS ns2.otherdomain.tld. diff --git a/models/dns.go b/models/dns.go index 689ad3192..1a906c687 100644 --- a/models/dns.go +++ b/models/dns.go @@ -71,8 +71,10 @@ type RecordConfig struct { SrvPriority uint16 `json:"srvpriority,omitempty"` SrvWeight uint16 `json:"srvweight,omitempty"` SrvPort uint16 `json:"srvport,omitempty"` + CaaTag string `json:"caatag,omitempty"` + CaaFlag uint8 `json:"caaflag,omitempty"` - CombinedTarget bool `json:"omit"` + CombinedTarget bool `json:"-"` Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. } @@ -90,6 +92,8 @@ func (r *RecordConfig) String() (content string) { content += fmt.Sprintf(" priority=%d", r.MxPreference) case "SOA": content = fmt.Sprintf("%s %s %s %d", r.Type, r.Name, r.Target, r.TTL) + case "CAA": + content += fmt.Sprintf(" caatag=%s caaflag=%d", r.CaaTag, r.CaaFlag) default: panic(fmt.Sprintf("rc.String rtype %v unimplemented", r.Type)) } @@ -138,6 +142,8 @@ func (r *RecordConfig) MergeToTarget() { r.SrvPriority = 0 r.SrvWeight = 0 r.SrvPort = 0 + r.CaaFlag = 0 + r.CaaTag = "" r.CombinedTarget = true } @@ -193,6 +199,10 @@ func (rc *RecordConfig) ToRR() dns.RR { rr.(*dns.SRV).Weight = rc.SrvWeight rr.(*dns.SRV).Port = rc.SrvPort rr.(*dns.SRV).Target = rc.Target + case dns.TypeCAA: + rr.(*dns.CAA).Flag = rc.CaaFlag + rr.(*dns.CAA).Tag = rc.CaaTag + rr.(*dns.CAA).Value = rc.Target case dns.TypeTXT: rr.(*dns.TXT).Txt = []string{rc.Target} default: diff --git a/models/dns_test.go b/models/dns_test.go index 77e3c489c..9474fb3c8 100644 --- a/models/dns_test.go +++ b/models/dns_test.go @@ -36,4 +36,19 @@ func TestRR(t *testing.T) { if found != expected { t.Errorf("RR expected (%#v) got (%#v)\n", expected, found) } + + experiment = RecordConfig{ + Type: "CAA", + Name: "@", + Target: "mailto:test@example.com", + TTL: 300, + NameFQDN: "example.com", + CaaTag: "iodef", + CaaFlags: 1, + } + expected = "example.com.\t300\tIN\tCAA\t1 iodef \"mailto:test@example.com\"" + found = experiment.ToRR().String() + if found != expected { + t.Errorf("RR expected (%#v) got (%#v)\n", expected, found) + } } diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index b91ee35cd..48ce93a99 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -118,6 +118,8 @@ function DefaultTTL(v) { } } +// CAA_CRITICAL: Critical CAA flag +var CAA_CRITICAL = 1<<0; // DnsProvider("providerName", 0) @@ -156,6 +158,18 @@ function ALIAS(name, target) { } } +// CAA(name,tag,value, recordModifiers...) +function CAA(name, tag, value){ + checkArgs([_.isString, _.isString, _.isString], arguments, "CAA expects (name, tag, value) plus optional flag as a meta argument") + + var mods = getModifiers(arguments,3) + mods.push({caatag: tag}); + + return function(d) { + addRecord(d,"CAA",name,value,mods) + } +} + // CNAME(name,target, recordModifiers...) function CNAME(name, target) { @@ -303,6 +317,9 @@ function addRecord(d,type,name,target,mods) { var m = mods[i] if (_.isFunction(m)) { m(rec); + } else if (_.isObject(m) && m.caatag) { + // caatag is a top level object, not in meta + rec.caatag = m.caatag; } else if (_.isObject(m)) { //convert transforms to strings if (m.transform && _.isArray(m.transform)){ @@ -312,6 +329,8 @@ function addRecord(d,type,name,target,mods) { _.extend(rec.meta,m); } else if (_.isNumber(m) && type == "MX") { rec.mxpreference = m; + } else if (_.isNumber(m) && type == "CAA") { + rec.caaflags |= m; } else { console.log("WARNING: Modifier type unsupported:", typeof m, "(Skipping!)"); } diff --git a/pkg/js/parse_tests/014-caa.js b/pkg/js/parse_tests/014-caa.js new file mode 100644 index 000000000..a7ab0f4f5 --- /dev/null +++ b/pkg/js/parse_tests/014-caa.js @@ -0,0 +1,13 @@ +D("foo.com","none", + // Allow letsencrypt to issue certificate for this domain + CAA("@","issue","letsencrypt.org"), + // Allow no CA to issue wildcard certificate for this domain + CAA("@","issuewild",";"), + // Report all violation to test@example.com. If CA does not support + // this record then refuse to issue any certificate + CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL), + // Optionally report violation to http://example.com + CAA("@", "iodef", "http://example.com"), + // Report violation to https://example.com + CAA("@", "iodef", "https://example.com", CAA_CRITICAL) +); diff --git a/pkg/js/parse_tests/014-caa.json b/pkg/js/parse_tests/014-caa.json new file mode 100644 index 000000000..c1060fb91 --- /dev/null +++ b/pkg/js/parse_tests/014-caa.json @@ -0,0 +1,43 @@ +{ + "registrars":[], + "dns_providers":[], + "domains":[ + { + "name":"foo.com", + "registrar":"none", + "dnsProviders":{}, + "records":[ + { + "type":"CAA", + "name":"@", + "target":"letsencrypt.org", + "caatag":"issue" + }, + { + "type":"CAA", + "name":"@", + "target":";", + "caatag":"issuewild" + }, + { + "type":"CAA", + "name":"@", + "target":"mailto:test@example.com", + "caatag":"iodef" + }, + { + "type":"CAA", + "name":"@", + "target":"http://example.com", + "caatag":"iodef" + }, + { + "type":"CAA", + "name":"@", + "target":"https://example.com", + "caatag":"iodef" + } + ] + } + ] +} diff --git a/pkg/js/static.go b/pkg/js/static.go index c4c825939..ec574f809 100644 --- a/pkg/js/static.go +++ b/pkg/js/static.go @@ -190,60 +190,63 @@ var _escData = map[string]*_escFile{ "/helpers.js": { local: "pkg/js/helpers.js", - size: 11080, + size: 11743, modtime: 0, compressed: ` -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 `, }, diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 3c10e1cd8..380fd846e 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -53,6 +53,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin "A": true, "AAAA": true, "CNAME": true, + "CAA": true, "IMPORT_TRANSFORM": false, "MX": true, "SRV": true, @@ -149,7 +150,7 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) { check(checkTarget(target)) case "SRV": check(checkTarget(target)) - case "TXT", "IMPORT_TRANSFORM": + case "TXT", "IMPORT_TRANSFORM", "CAA": default: if rec.Metadata["orig_custom_type"] != "" { //it is a valid custom type. We perform no validation on target @@ -206,7 +207,7 @@ func importTransform(srcDomain, dstDomain *models.DomainConfig, transforms []tra r := newRec() r.Target = transformCNAME(r.Target, srcDomain.Name, dstDomain.Name) dstDomain.Records = append(dstDomain.Records, r) - case "MX", "NS", "SRV", "TXT": + case "MX", "NS", "SRV", "TXT", "CAA": // Not imported. continue default: @@ -281,6 +282,10 @@ func NormalizeAndValidateConfig(config *models.DNSConfig) (errs []error) { if rec.Name, err = transform.PtrNameMagic(rec.Name, domain.Name); err != nil { errs = append(errs, err) } + } else if rec.Type == "CAA" { + if rec.CaaTag != "issue" && rec.CaaTag != "issuewild" && rec.CaaTag != "iodef" { + errs = append(errs, fmt.Errorf("CAA tag %s is invalid", rec.CaaTag)) + } } // Populate FQDN: rec.NameFQDN = dnsutil.AddOrigin(rec.Name, domain.Name) @@ -357,6 +362,7 @@ func checkProviderCapabilities(dc *models.DomainConfig, pList []*models.DNSProvi {"ALIAS", providers.CanUseAlias}, {"PTR", providers.CanUsePTR}, {"SRV", providers.CanUseSRV}, + {"CAA", providers.CanUseCAA}, } for _, ty := range types { hasAny := false diff --git a/pkg/normalize/validate_test.go b/pkg/normalize/validate_test.go index 65a77c664..7cb296c58 100644 --- a/pkg/normalize/validate_test.go +++ b/pkg/normalize/validate_test.go @@ -173,3 +173,21 @@ func TestCNAMEMutex(t *testing.T) { }) } } + +func TestCAAValidation(t *testing.T) { + config := &models.DNSConfig{ + Domains: []*models.DomainConfig{ + { + Name: "example.com", + Registrar: "BIND", + Records: []*models.RecordConfig{ + {Name: "@", Type: "CAA", CaaTag: "invalid", Target: "example.com"}, + }, + }, + }, + } + errs := NormalizeAndValidateConfig(config) + if len(errs) != 1 { + t.Error("Expect error on invalid CAA but got none") + } +} diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index 01000dfaf..b8d1fffda 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -47,7 +47,7 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers } func init() { - providers.RegisterDomainServiceProviderType("BIND", initBind, providers.CanUsePTR, providers.CanUseSRV) + providers.RegisterDomainServiceProviderType("BIND", initBind, providers.CanUsePTR, providers.CanUseSRV, providers.CanUseCAA) } type SoaInfo struct { @@ -93,6 +93,10 @@ func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordCo rc.Target = v.A.String() case *dns.AAAA: rc.Target = v.AAAA.String() + case *dns.CAA: + rc.CaaTag = v.Tag + rc.CaaFlag = v.Flag + rc.Target = v.Value case *dns.CNAME: rc.Target = v.Target case *dns.MX: diff --git a/providers/bind/prettyzone.go b/providers/bind/prettyzone.go index 77afccd39..8633fa3c5 100644 --- a/providers/bind/prettyzone.go +++ b/providers/bind/prettyzone.go @@ -67,6 +67,19 @@ func (z *zoneGenData) Less(i, j int) bool { if pa != pb { return pa < pb } + case dns.TypeCAA: + ta2, tb2 := a.(*dns.CAA), b.(*dns.CAA) + // sort by tag + pa, pb := ta2.Tag, tb2.Tag + if pa != pb { + return pa < pb + } + // then flag + fa, fb := ta2.Flag, tb2.Flag + if fa != fb { + // flag set goes before ones without flag set + return fa > fb + } default: panic(fmt.Sprintf("zoneGenData Less: unimplemented rtype %v", dns.TypeToString[rrtypeA])) } diff --git a/providers/bind/prettyzone_test.go b/providers/bind/prettyzone_test.go index 12ba47d3d..fabbc9928 100644 --- a/providers/bind/prettyzone_test.go +++ b/providers/bind/prettyzone_test.go @@ -181,6 +181,33 @@ var testdataZFSRV = `$TTL 300 IN SRV 10 10 9999 foo.com. ` +func TestWriteZoneFileCaa(t *testing.T) { + //exhibits explicit ttls and long name + r1, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issuewild ";"`) + r2, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issue "letsencrypt.org"`) + r3, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "http://example.com"`) + r4, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.com"`) + r5, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.net"`) + r6, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "mailto:example.com"`) + buf := &bytes.Buffer{} + WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org") + if buf.String() != testdataZFCAA { + t.Log(buf.String()) + t.Log(testdataZFCAA) + t.Fatalf("Zone file does not match.") + } + parseAndRegen(t, buf, testdataZFCAA) +} + +var testdataZFCAA = `$TTL 300 +@ IN CAA 1 iodef "http://example.com" + IN CAA 1 iodef "mailto:example.com" + IN CAA 0 iodef "https://example.com" + IN CAA 0 iodef "https://example.net" + IN CAA 0 issue "letsencrypt.org" + IN CAA 0 issuewild ";" +` + func TestWriteZoneFileOrder(t *testing.T) { var records []dns.RR for i, td := range []string{ diff --git a/providers/google/google.go b/providers/google/google.go index 246088ec9..566105c72 100644 --- a/providers/google/google.go +++ b/providers/google/google.go @@ -15,7 +15,7 @@ import ( ) func init() { - providers.RegisterDomainServiceProviderType("GCLOUD", New, providers.CanUsePTR, providers.CanUseSRV) + providers.RegisterDomainServiceProviderType("GCLOUD", New, providers.CanUsePTR, providers.CanUseSRV, providers.CanUseCAA) } type gcloud struct { diff --git a/providers/providers.go b/providers/providers.go index 001313958..617b6a4ab 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -47,6 +47,8 @@ const ( CanUsePTR // CanUseSRV indicates the provider can handle SRV records CanUseSRV + // CanUseCAA indicates the provider can handle CAA records + CanUseCAA ) func ProviderHasCabability(pType string, cap Capability) bool {