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
This commit is contained in:
Tom Limoncelli 2017-07-25 14:59:40 -04:00 committed by GitHub
parent 1a84edbe9c
commit 2f0f5330fc
16 changed files with 283 additions and 58 deletions

View file

@ -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 %}

View file

@ -236,6 +236,13 @@ func srv(name string, priority, weight, port uint16, target string) *rec {
return r 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 { func makeRec(name, target, typ string) *rec {
return &rec{ return &rec{
Name: name, 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 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), 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. //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.")), //tc("IDN pre-punycoded", cname("xn--o-0gab", "xn--o-0gab.xn--o-0gab.")),
} }

View file

@ -1,4 +1,4 @@
$TTL 300 $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 ns1.otherdomain.tld.
IN NS ns2.otherdomain.tld. IN NS ns2.otherdomain.tld.

View file

@ -71,8 +71,10 @@ type RecordConfig struct {
SrvPriority uint16 `json:"srvpriority,omitempty"` SrvPriority uint16 `json:"srvpriority,omitempty"`
SrvWeight uint16 `json:"srvweight,omitempty"` SrvWeight uint16 `json:"srvweight,omitempty"`
SrvPort uint16 `json:"srvport,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. 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) content += fmt.Sprintf(" priority=%d", r.MxPreference)
case "SOA": case "SOA":
content = fmt.Sprintf("%s %s %s %d", r.Type, r.Name, r.Target, r.TTL) 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: default:
panic(fmt.Sprintf("rc.String rtype %v unimplemented", r.Type)) panic(fmt.Sprintf("rc.String rtype %v unimplemented", r.Type))
} }
@ -138,6 +142,8 @@ func (r *RecordConfig) MergeToTarget() {
r.SrvPriority = 0 r.SrvPriority = 0
r.SrvWeight = 0 r.SrvWeight = 0
r.SrvPort = 0 r.SrvPort = 0
r.CaaFlag = 0
r.CaaTag = ""
r.CombinedTarget = true r.CombinedTarget = true
} }
@ -193,6 +199,10 @@ func (rc *RecordConfig) ToRR() dns.RR {
rr.(*dns.SRV).Weight = rc.SrvWeight rr.(*dns.SRV).Weight = rc.SrvWeight
rr.(*dns.SRV).Port = rc.SrvPort rr.(*dns.SRV).Port = rc.SrvPort
rr.(*dns.SRV).Target = rc.Target 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: case dns.TypeTXT:
rr.(*dns.TXT).Txt = []string{rc.Target} rr.(*dns.TXT).Txt = []string{rc.Target}
default: default:

View file

@ -36,4 +36,19 @@ func TestRR(t *testing.T) {
if found != expected { if found != expected {
t.Errorf("RR expected (%#v) got (%#v)\n", expected, found) 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)
}
} }

View file

@ -118,6 +118,8 @@ function DefaultTTL(v) {
} }
} }
// CAA_CRITICAL: Critical CAA flag
var CAA_CRITICAL = 1<<0;
// DnsProvider("providerName", 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...) // CNAME(name,target, recordModifiers...)
function CNAME(name, target) { function CNAME(name, target) {
@ -303,6 +317,9 @@ function addRecord(d,type,name,target,mods) {
var m = mods[i] var m = mods[i]
if (_.isFunction(m)) { if (_.isFunction(m)) {
m(rec); 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)) { } else if (_.isObject(m)) {
//convert transforms to strings //convert transforms to strings
if (m.transform && _.isArray(m.transform)){ if (m.transform && _.isArray(m.transform)){
@ -312,6 +329,8 @@ function addRecord(d,type,name,target,mods) {
_.extend(rec.meta,m); _.extend(rec.meta,m);
} else if (_.isNumber(m) && type == "MX") { } else if (_.isNumber(m) && type == "MX") {
rec.mxpreference = m; rec.mxpreference = m;
} else if (_.isNumber(m) && type == "CAA") {
rec.caaflags |= m;
} else { } else {
console.log("WARNING: Modifier type unsupported:", typeof m, "(Skipping!)"); console.log("WARNING: Modifier type unsupported:", typeof m, "(Skipping!)");
} }

View file

@ -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)
);

View file

@ -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"
}
]
}
]
}

View file

@ -190,60 +190,63 @@ var _escData = map[string]*_escFile{
"/helpers.js": { "/helpers.js": {
local: "pkg/js/helpers.js", local: "pkg/js/helpers.js",
size: 11080, size: 11743,
modtime: 0, modtime: 0,
compressed: ` compressed: `
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
`, `,
}, },

View file

@ -53,6 +53,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
"A": true, "A": true,
"AAAA": true, "AAAA": true,
"CNAME": true, "CNAME": true,
"CAA": true,
"IMPORT_TRANSFORM": false, "IMPORT_TRANSFORM": false,
"MX": true, "MX": true,
"SRV": true, "SRV": true,
@ -149,7 +150,7 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
check(checkTarget(target)) check(checkTarget(target))
case "SRV": case "SRV":
check(checkTarget(target)) check(checkTarget(target))
case "TXT", "IMPORT_TRANSFORM": case "TXT", "IMPORT_TRANSFORM", "CAA":
default: default:
if rec.Metadata["orig_custom_type"] != "" { if rec.Metadata["orig_custom_type"] != "" {
//it is a valid custom type. We perform no validation on target //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 := newRec()
r.Target = transformCNAME(r.Target, srcDomain.Name, dstDomain.Name) r.Target = transformCNAME(r.Target, srcDomain.Name, dstDomain.Name)
dstDomain.Records = append(dstDomain.Records, r) dstDomain.Records = append(dstDomain.Records, r)
case "MX", "NS", "SRV", "TXT": case "MX", "NS", "SRV", "TXT", "CAA":
// Not imported. // Not imported.
continue continue
default: default:
@ -281,6 +282,10 @@ func NormalizeAndValidateConfig(config *models.DNSConfig) (errs []error) {
if rec.Name, err = transform.PtrNameMagic(rec.Name, domain.Name); err != nil { if rec.Name, err = transform.PtrNameMagic(rec.Name, domain.Name); err != nil {
errs = append(errs, err) 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: // Populate FQDN:
rec.NameFQDN = dnsutil.AddOrigin(rec.Name, domain.Name) rec.NameFQDN = dnsutil.AddOrigin(rec.Name, domain.Name)
@ -357,6 +362,7 @@ func checkProviderCapabilities(dc *models.DomainConfig, pList []*models.DNSProvi
{"ALIAS", providers.CanUseAlias}, {"ALIAS", providers.CanUseAlias},
{"PTR", providers.CanUsePTR}, {"PTR", providers.CanUsePTR},
{"SRV", providers.CanUseSRV}, {"SRV", providers.CanUseSRV},
{"CAA", providers.CanUseCAA},
} }
for _, ty := range types { for _, ty := range types {
hasAny := false hasAny := false

View file

@ -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")
}
}

View file

@ -47,7 +47,7 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("BIND", initBind, providers.CanUsePTR, providers.CanUseSRV) providers.RegisterDomainServiceProviderType("BIND", initBind, providers.CanUsePTR, providers.CanUseSRV, providers.CanUseCAA)
} }
type SoaInfo struct { type SoaInfo struct {
@ -93,6 +93,10 @@ func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordCo
rc.Target = v.A.String() rc.Target = v.A.String()
case *dns.AAAA: case *dns.AAAA:
rc.Target = v.AAAA.String() rc.Target = v.AAAA.String()
case *dns.CAA:
rc.CaaTag = v.Tag
rc.CaaFlag = v.Flag
rc.Target = v.Value
case *dns.CNAME: case *dns.CNAME:
rc.Target = v.Target rc.Target = v.Target
case *dns.MX: case *dns.MX:

View file

@ -67,6 +67,19 @@ func (z *zoneGenData) Less(i, j int) bool {
if pa != pb { if pa != pb {
return 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: default:
panic(fmt.Sprintf("zoneGenData Less: unimplemented rtype %v", dns.TypeToString[rrtypeA])) panic(fmt.Sprintf("zoneGenData Less: unimplemented rtype %v", dns.TypeToString[rrtypeA]))
} }

View file

@ -181,6 +181,33 @@ var testdataZFSRV = `$TTL 300
IN SRV 10 10 9999 foo.com. 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) { func TestWriteZoneFileOrder(t *testing.T) {
var records []dns.RR var records []dns.RR
for i, td := range []string{ for i, td := range []string{

View file

@ -15,7 +15,7 @@ import (
) )
func init() { func init() {
providers.RegisterDomainServiceProviderType("GCLOUD", New, providers.CanUsePTR, providers.CanUseSRV) providers.RegisterDomainServiceProviderType("GCLOUD", New, providers.CanUsePTR, providers.CanUseSRV, providers.CanUseCAA)
} }
type gcloud struct { type gcloud struct {

View file

@ -47,6 +47,8 @@ const (
CanUsePTR CanUsePTR
// CanUseSRV indicates the provider can handle SRV records // CanUseSRV indicates the provider can handle SRV records
CanUseSRV CanUseSRV
// CanUseCAA indicates the provider can handle CAA records
CanUseCAA
) )
func ProviderHasCabability(pType string, cap Capability) bool { func ProviderHasCabability(pType string, cap Capability) bool {