From 6624e237ed8793afabfe07665dec978dfbaf0ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20S=C3=BCtterlin?= Date: Sat, 20 Sep 2025 13:01:51 +0200 Subject: [PATCH] CAA: Support issuemail / issuevmc tag in CAA builder --- commands/types/dnscontrol.d.ts | 12 +++++- .../domain-modifiers/CAA.md | 6 ++- .../domain-modifiers/CAA_BUILDER.md | 12 ++++++ models/t_caa.go | 7 +++- pkg/js/helpers.js | 40 +++++++++++++++++-- pkg/normalize/validate.go | 5 ++- 6 files changed, 73 insertions(+), 9 deletions(-) diff --git a/commands/types/dnscontrol.d.ts b/commands/types/dnscontrol.d.ts index ab2a83029..289b197f0 100644 --- a/commands/types/dnscontrol.d.ts +++ b/commands/types/dnscontrol.d.ts @@ -384,6 +384,10 @@ declare function AZURE_ALIAS(name: string, type: "A" | "AAAA" | "CNAME", target: * 1. `"issue"` * 2. `"issuewild"` * 3. `"iodef"` + * 4. `"contactemail"` + * 5. `"contactphone"` + * 6. `"issuemail"` + * 7. `"issuevmc"` * * Value is a string. The format of the contents is different depending on the tag. DNSControl will handle any escaping or quoting required, similar to TXT records. For example use `CAA("@", "issue", "letsencrypt.org")` rather than `CAA("@", "issue", "\"letsencrypt.org\"")`. * @@ -406,7 +410,7 @@ declare function AZURE_ALIAS(name: string, type: "A" | "AAAA" | "CNAME", target: * * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/caa */ -declare function CAA(name: string, tag: "issue" | "issuewild" | "iodef", value: string, ...modifiers: RecordModifier[]): DomainModifier; +declare function CAA(name: string, tag: "issue" | "issuewild" | "iodef" | "contactemail" | "contactphone" | "issuemail" | "issuevmc", value: string, ...modifiers: RecordModifier[]): DomainModifier; /** * DNSControl contains a `CAA_BUILDER` which can be used to simply create @@ -503,11 +507,15 @@ declare function CAA(name: string, tag: "issue" | "issuewild" | "iodef", value: * * `issue_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`) * * `issuewild:` An array of CAs which are allowed to issue wildcard certificates. (Can be simply `"none"` to refuse issuing wildcard certificates for all CAs) * * `issuewild_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`) + * * `issuevmc:` An array of CAs which are allowed to issue VMC certificates. (Use `"none"` to refuse all CAs) + * * `issuevmc_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`) + * * `issuemail:` An array of CAs which are allowed to issue email certificates. (Use `"none"` to refuse all CAs) + * * `issuemail_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`) * * `ttl:` Input for `TTL` method (optional) * * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/caa_builder */ -declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critical?: boolean; issue: string[]|string; issue_critical?: boolean; issuewild: string[]|string; issuewild_critical?: boolean; ttl?: Duration }): DomainModifier; +declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critical?: boolean; issue: string[]|string; issue_critical?: boolean; issuewild: string[]|string; issuewild_critical?: boolean; issuevmc: string[]|string; issuevmc_critical?: boolean; issuemail: string[]|string; issuemail_critical?: boolean; ttl?: Duration }): DomainModifier; /** * WARNING: Cloudflare is removing this feature and replacing it with a new diff --git a/documentation/language-reference/domain-modifiers/CAA.md b/documentation/language-reference/domain-modifiers/CAA.md index 22428afb1..ba8b8a2e3 100644 --- a/documentation/language-reference/domain-modifiers/CAA.md +++ b/documentation/language-reference/domain-modifiers/CAA.md @@ -7,7 +7,7 @@ parameters: - modifiers... parameter_types: name: string - tag: '"issue" | "issuewild" | "iodef"' + tag: '"issue" | "issuewild" | "iodef" | "contactemail" | "contactphone" | "issuemail" | "issuevmc"' value: string "modifiers...": RecordModifier[] --- @@ -18,6 +18,10 @@ Tag can be one of 1. `"issue"` 2. `"issuewild"` 3. `"iodef"` +4. `"contactemail"` +5. `"contactphone"` +6. `"issuemail"` +7. `"issuevmc"` Value is a string. The format of the contents is different depending on the tag. DNSControl will handle any escaping or quoting required, similar to TXT records. For example use `CAA("@", "issue", "letsencrypt.org")` rather than `CAA("@", "issue", "\"letsencrypt.org\"")`. diff --git a/documentation/language-reference/domain-modifiers/CAA_BUILDER.md b/documentation/language-reference/domain-modifiers/CAA_BUILDER.md index 078d180a7..0423a68b5 100644 --- a/documentation/language-reference/domain-modifiers/CAA_BUILDER.md +++ b/documentation/language-reference/domain-modifiers/CAA_BUILDER.md @@ -8,6 +8,10 @@ parameters: - issue_critical - issuewild - issuewild_critical + - issuevmc + - issuevmc_critical + - issuemail + - issuemail_critical - ttl parameters_object: true parameter_types: @@ -18,6 +22,10 @@ parameter_types: issue_critical: boolean? issuewild: string[]|string issuewild_critical: boolean? + issuevmc: string[]|string + issuevmc_critical: boolean? + issuemail: string[]|string + issuemail_critical: boolean? ttl: Duration? --- @@ -123,4 +131,8 @@ which in turns yield the following records: * `issue_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`) * `issuewild:` An array of CAs which are allowed to issue wildcard certificates. (Can be simply `"none"` to refuse issuing wildcard certificates for all CAs) * `issuewild_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`) +* `issuevmc:` An array of CAs which are allowed to issue VMC certificates. (Use `"none"` to refuse all CAs) +* `issuevmc_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`) +* `issuemail:` An array of CAs which are allowed to issue email certificates. (Use `"none"` to refuse all CAs) +* `issuemail_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`) * `ttl:` Input for `TTL` method (optional) diff --git a/models/t_caa.go b/models/t_caa.go index b3743e68f..73ced3a13 100644 --- a/models/t_caa.go +++ b/models/t_caa.go @@ -2,6 +2,7 @@ package models import ( "fmt" + "slices" "strconv" ) @@ -19,8 +20,10 @@ func (rc *RecordConfig) SetTargetCAA(flag uint8, tag string, target string) erro panic("assertion failed: SetTargetCAA called when .Type is not CAA") } - if tag != "issue" && tag != "issuewild" && tag != "iodef" { - return fmt.Errorf("CAA tag (%v) is not one of issue/issuewild/iodef", tag) + // Per: https://www.iana.org/assignments/pkix-parameters/pkix-parameters.xhtml#caa-properties excluding reserved tags + allowedTags := []string{"issue", "issuewild", "iodef", "contactemail", "contactphone", "issuemail", "issuevmc"} + if !slices.Contains(allowedTags, tag) { + return fmt.Errorf("CAA tag (%v) is not one of the valid types.", tag) } return nil diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index 4f54504ba..d2239e198 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -1691,6 +1691,12 @@ function SPF_BUILDER(value) { // iodef_critical: Boolean if sending report is required/critical. If not supported, certificate should be refused. (optional) // issue: List of CAs which are allowed to issue certificates for the domain (creates one record for each), or the string 'none'. // issuewild: List of allowed CAs which can issue wildcard certificates for this domain, or the string 'none'. (creates one record for each) +// issuevmc: List of allowed CAs which can issue VMC certificates for this domain, or the string 'none'. (creates one record for each) +// issuemail: List of allowed CAs which can issue email certificates for this domain, or the string 'none'. (creates one record for each) +// issue_critical: Boolean if issue entries are critical. If not supported, certificate should be refused. (optional) +// issuewild_critical: Boolean if issuewild entries are critical. If not supported, certificate should be refused. (optional) +// issuevmc_critical: Boolean if issuevmc entries are critical. If not supported, certificate should be refused. (optional) +// issuemail_critical: Boolean if issuemail entries are critical. If not supported, certificate should be refused. (optional) // ttl: The time for TTL, integer or string. (default: not defined, using DefaultTTL) function CAA_BUILDER(value) { @@ -1700,15 +1706,21 @@ function CAA_BUILDER(value) { if (value.issue && value.issue == 'none') value.issue = [';']; if (value.issuewild && value.issuewild == 'none') value.issuewild = [';']; + if (value.issuevmc && value.issuevmc == 'none') value.issuevmc = [';']; + if (value.issuemail && value.issuemail == 'none') value.issuemail = [';']; if ( - (!value.issue && !value.issuewild) || + (!value.issue && !value.issuewild && !value.issuevmc && !value.issuemail) || (value.issue && value.issue.length == 0 && value.issuewild && - value.issuewild.length == 0) + value.issuewild.length == 0 && + value.issuevmc && + value.issuevmc.length == 0 && + value.issuemail && + value.issuemail.length == 0) ) { - throw 'CAA_BUILDER requires at least one entry at issue or issuewild'; + throw 'CAA_BUILDER requires at least one entry at issue, issuewild, issuevmc or issuemail'; } var CAA_TTL = function () {}; @@ -1747,6 +1759,28 @@ function CAA_BUILDER(value) { ); } + if (value.issuevmc) { + var flag = function () {}; + if (value.issuevmc_critical) { + flag = CAA_CRITICAL; + } + for (var i = 0, len = value.issuevmc.length; i < len; i++) + r.push( + CAA(value.label, 'issuevmc', value.issuevmc[i], flag, CAA_TTL) + ); + } + + if (value.issuemail) { + var flag = function () {}; + if (value.issuemail_critical) { + flag = CAA_CRITICAL; + } + for (var i = 0, len = value.issuemail.length; i < len; i++) + r.push( + CAA(value.label, 'issuemail', value.issuemail[i], flag, CAA_TTL) + ); + } + return r; } diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 4500d0598..882617cd2 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net" + "slices" "sort" "strconv" "strings" @@ -442,7 +443,9 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) { } rec.SetLabel(name, domain.Name) } else if rec.Type == "CAA" { - if rec.CaaTag != "issue" && rec.CaaTag != "issuewild" && rec.CaaTag != "iodef" && rec.CaaTag != "issuemail" { + // Per: https://www.iana.org/assignments/pkix-parameters/pkix-parameters.xhtml#caa-properties excluding reserved tags + allowedTags := []string{"issue", "issuewild", "iodef", "contactemail", "contactphone", "issuemail", "issuevmc"} + if !slices.Contains(allowedTags, rec.CaaTag) { errs = append(errs, fmt.Errorf("CAA tag %s is invalid", rec.CaaTag)) } } else if rec.Type == "TLSA" {