Skip to content

Commit c9ca0eb

Browse files
committed
VERCEL: handle CAA whitespace edge case
1 parent ea0c0f0 commit c9ca0eb

File tree

3 files changed

+100
-8
lines changed

3 files changed

+100
-8
lines changed

integrationTest/integration_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,20 @@ func makeTests() []*TestGroup {
13471347
tc("simple", aghAAAAPassthrough("foo", "")),
13481348
),
13491349

1350+
// VERCEL features(?)
1351+
1352+
// Turns out that Vercel does support whitespace in the CAA record,
1353+
// but it only supports `cansignhttpexchanges` field, all other fields,
1354+
// `validationmethods`, `accounturi` are not supported
1355+
//
1356+
// In order to test the `CAA whitespace` capabilities and quirks, let's go!
1357+
testgroup("VERCEL CAA whitespace - cansignhttpexchanges",
1358+
only(
1359+
"VERCEL",
1360+
),
1361+
tc("CAA whitespace - cansignhttpexchanges", caa("@", 128, "issue", "digicert.com; cansignhttpexchanges=yes")),
1362+
),
1363+
13501364
//// IGNORE* features
13511365

13521366
// Narrative: You're basically done now. These remaining tests

providers/vercel/auditrecords.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package vercel
22

33
import (
4-
"errors"
4+
"fmt"
5+
"strings"
56

67
"github.com/StackExchange/dnscontrol/v4/models"
78
"github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
@@ -30,23 +31,39 @@ func AuditRecords(records []*models.RecordConfig) []error {
3031
// last verified 2025-11-22
3132
// bad_request - invalid_value - The specified value is not a fully qualified domain name.
3233
a.Add("CAA", rejectif.CaaHasEmptyTarget)
33-
a.Add("CAA", rejectifCaaTargetIsSemicolon)
3434

3535
// last verified 2025-11-22
3636
// Vercel misidentified extra fields in CAA record `0 issue letsencrypt.org; validationmethods=dns-01; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234`
3737
// as "cansignhttpexchanges", and add extra incorrect validation on the value
38-
// let's ignore all whitespace for now, i should report this to Vercel though, as
39-
// it uses NS1 as its provder and NS1 definitly allows it.
4038
//
4139
// invalid_value - Unexpected "cansignhttpexchanges" value.
42-
a.Add("CAA", rejectif.CaaTargetContainsWhitespace)
40+
a.Add("CAA", rejectifCaaTargetContainsUnsupportedFields)
4341

4442
return a.Audit(records)
4543
}
4644

47-
func rejectifCaaTargetIsSemicolon(rc *models.RecordConfig) error {
48-
if rc.GetTargetField() == ";" {
49-
return errors.New("caa target cannot be ';'")
45+
func rejectifCaaTargetContainsUnsupportedFields(rc *models.RecordConfig) error {
46+
target := rc.GetTargetField()
47+
if !strings.Contains(target, ";") {
48+
return nil
49+
}
50+
51+
parts := strings.Split(target, ";")
52+
// The first part is the domain, which we only check length for now
53+
if len(parts[0]) < 1 {
54+
return fmt.Errorf("caa target domain is empty")
55+
}
56+
for _, part := range parts[1:] {
57+
part = strings.TrimSpace(part)
58+
if part == "" {
59+
continue
60+
}
61+
// Check if the part starts with "cansignhttpexchanges"
62+
// It can be just "cansignhttpexchanges" or "cansignhttpexchanges=..."
63+
if part == "cansignhttpexchanges" || strings.HasPrefix(part, "cansignhttpexchanges=") {
64+
continue
65+
}
66+
return fmt.Errorf("caa target contains unsupported field: %s", part)
5067
}
5168
return nil
5269
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package vercel
2+
3+
import (
4+
"testing"
5+
6+
"github.com/StackExchange/dnscontrol/v4/models"
7+
)
8+
9+
func TestCaaTargetContainsUnsupportedFields(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
target string
13+
wantErr bool
14+
}{
15+
{
16+
name: "simple domain",
17+
target: "letsencrypt.org",
18+
wantErr: false,
19+
},
20+
{
21+
name: "with cansignhttpexchanges",
22+
target: "digicert.com; cansignhttpexchanges=yes",
23+
wantErr: false,
24+
},
25+
{
26+
name: "with empty domain",
27+
target: ";",
28+
wantErr: true,
29+
},
30+
{
31+
name: "with validationmethods",
32+
target: "letsencrypt.org; validationmethods=dns-01",
33+
wantErr: true,
34+
},
35+
{
36+
name: "with accounturi",
37+
target: "letsencrypt.org; accounturi=https://example.com",
38+
wantErr: true,
39+
},
40+
{
41+
name: "with multiple params including allowed",
42+
target: "letsencrypt.org; cansignhttpexchanges; validationmethods=dns-01",
43+
wantErr: true,
44+
},
45+
{
46+
name: "with unknown param",
47+
target: "letsencrypt.org; foo=bar",
48+
wantErr: true,
49+
},
50+
}
51+
52+
for _, tt := range tests {
53+
t.Run(tt.name, func(t *testing.T) {
54+
rc := &models.RecordConfig{}
55+
rc.SetTarget(tt.target)
56+
if err := rejectifCaaTargetContainsUnsupportedFields(rc); (err != nil) != tt.wantErr {
57+
t.Errorf("caaTargetContainsUnsupportedFields() error = %v, wantErr %v", err, tt.wantErr)
58+
}
59+
})
60+
}
61+
}

0 commit comments

Comments
 (0)