├── .gitignore ├── AUTHORS ├── tools └── tools.go ├── testdata └── openspf │ ├── README.md │ ├── tnnnn-spftest-org-db │ ├── rfc4408-tests-LICENSE │ ├── rfc7208-tests-LICENSE │ ├── pyspf-tests.yml │ ├── rfc4408-tests-CHANGES │ ├── schema.rng │ ├── rfc7208-tests-CHANGES │ └── nnn-spf1-test-mailzone-com-db ├── doc.go ├── go.mod ├── example_test.go ├── hook.go ├── .github └── workflows │ └── release.yaml ├── LICENSE ├── resolver.go ├── resulttype_enumer.go ├── result.go ├── go.sum ├── README.md ├── ptr.go ├── dns_helpers.go ├── cmd └── spf │ └── main.go ├── spf_test.go ├── macro.go ├── spf.go ├── docs ├── rfc8616.txt ├── rfc7372.txt └── rfc8553.txt └── mechanism.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | cmd/spf/spf 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Steve Atkins 2 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | package tools 3 | 4 | 5 | import ( 6 | _ "github.com/alvaroloes/enumer" 7 | ) 8 | -------------------------------------------------------------------------------- /testdata/openspf/README.md: -------------------------------------------------------------------------------- 1 | # Test data 2 | 3 | These test vectors were taken unchanged from what's left of the [openspf repo](http://www.open-spf.org/svn/project/test-suite/) 4 | left on the open-spf.org mirror. 5 | 6 | `pyspf-tests.yml` and `rfc7208-tests.yml` are used unchanged, with 7 | some creative parsing in `spf_test.yml` to convert it to a form we can 8 | use. 9 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package spf implements an SPF checker to evaluate whether or not an email 3 | messages passes a published SPF (Sender Policy Framework) policy. 4 | 5 | It implements all of the SPF checker protocol as described in RFC 7208, including 6 | macros and PTR checks, and passes 100% of the openspf and pyspf test suites. 7 | 8 | A DNS stub resolver is included, but can be replaced by anything that implements 9 | the spf.Resolver interface. 10 | 11 | The Hook interface can be used to hook into the check_host function to see more 12 | details about why a policy passes or fails. 13 | */ 14 | package spf 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wttw/spf 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/alvaroloes/enumer v1.1.2 9 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 10 | github.com/mattn/go-colorable v0.1.6 11 | github.com/mattn/go-isatty v0.0.12 12 | github.com/miekg/dns v1.1.62 13 | gopkg.in/yaml.v2 v2.2.8 14 | ) 15 | 16 | require ( 17 | github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 // indirect 18 | golang.org/x/mod v0.21.0 // indirect 19 | golang.org/x/net v0.30.0 // indirect 20 | golang.org/x/sync v0.8.0 // indirect 21 | golang.org/x/sys v0.26.0 // indirect 22 | golang.org/x/tools v0.26.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package spf_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/wttw/spf" 9 | ) 10 | 11 | func ExampleCheck() { 12 | ip := net.ParseIP("8.8.8.8") 13 | result, _ := spf.Check(context.Background(), ip, "steve@aol.com", "aol.com") 14 | fmt.Println(result) 15 | // Output: softfail 16 | } 17 | 18 | func ExampleChecker_SPF() { 19 | ip := net.ParseIP("8.8.8.8") 20 | c := spf.NewChecker() 21 | c.Hostname = "mail.example.com" 22 | result := c.SPF(context.Background(), ip, "steve@aol.com", "aol.com") 23 | fmt.Printf("Authentication-Results: %s\n", result.AuthenticationResults()) 24 | // Output: Authentication-Results: mail.example.com; spf=softfail smtp.helo=aol.com 25 | } 26 | -------------------------------------------------------------------------------- /hook.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import "github.com/miekg/dns" 4 | 5 | // Hook allows a caller to intercept the SPF check process at various points 6 | // through it's execution. 7 | type Hook interface { 8 | Dns(r *dns.Msg, m *dns.Msg, err error) // a dns record was looked up 9 | Record(record, domain string) // an SPF record is about to be processed 10 | RecordResult(domain string, result *Result) // an SPF record has completed processing 11 | Macro(before, after string, err error) // a macro has been expanded 12 | Mechanism(domain string, index int, mechanism Mechanism, result *Result) // an SPF mechanism has provided a result 13 | Redirect(target string) // an SPF redirect modifier is about to be executed 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | releases-matrix: 7 | name: Release Go Binary 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | # build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 12 | goos: [linux, windows, darwin] 13 | goarch: ["386", amd64, arm64] 14 | exclude: 15 | - goarch: "386" 16 | goos: darwin 17 | - goarch: arm64 18 | goos: windows 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: wangyoucao577/go-release-action@v1.25 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | goos: ${{ matrix.goos }} 25 | goarch: ${{ matrix.goarch }} 26 | project_path: "./cmd/spf" 27 | binary_name: "spf" 28 | extra_files: LICENSE README.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Turscar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /testdata/openspf/tnnnn-spftest-org-db: -------------------------------------------------------------------------------- 1 | t0000.spftest.org. IN TXT "v=spf1 -all" 2 | t0001.spftest.org. IN TXT "v=spf1 redirect=fail.t0001.spftest.org +all" 3 | fail.t0001.spftest.org. IN TXT "v=spf1 -all" 4 | t0002.spftest.org. IN TXT "v=spf1 include:fail.t0002.spftest.org +all" 5 | fail.t0002.spftest.org. IN TXT "v=spf1 -all" 6 | t0003.spftest.org. IN TXT "v=spf1 +all redirect=fail.t0003.spftest.org" 7 | fail.t0003.spftest.org. IN TXT "v=spf1 -all" 8 | t0004.spftest.org. IN TXT "v=spf1 +all include:fail.t0004.spftest.org" 9 | fail.t0004.spftest.org. IN TXT "v=spf1 -all" 10 | t0005.spftest.org. IN TXT "v=spf1 ip4:207.178.132.16/29 ip4:207.178.128.0/24 a mx" 11 | t0006.spftest.org. IN TXT "v=spf1 ip4:207.178.132.16/29" 12 | t0007.spftest.org. IN TXT "v=spf1 ip4:207.178.128.0/24" 13 | t0008.spftest.org. IN TXT "v=spf1 a" 14 | ;t0009.spftest.org. RCODE=SERVFAIL -------------------------------------------------------------------------------- /testdata/openspf/rfc4408-tests-LICENSE: -------------------------------------------------------------------------------- 1 | The RFC 4408 test-suite (rfc4408-tests.yml) is 2 | (C) 2006-2008 Stuart D Gathman 3 | 2007-2008 Julian Mehnle 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The names of the authors may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 15 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 21 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 23 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /testdata/openspf/rfc7208-tests-LICENSE: -------------------------------------------------------------------------------- 1 | The RFC 7208 test-suite (rfc7208-tests.yml) is 2 | (C) 2006-2014 Stuart D Gathman 3 | 2014 Scott Kitterman 4 | 2007-2008 Julian Mehnle 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 3. The names of the authors may not be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /resolver.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | // ResolvConf holds the path to a resolv.conf(5) format file used to 11 | // configure DefaultResolver. 12 | var ResolvConf = "/etc/resolv.conf" 13 | 14 | // Resolver is used for all DNS lookups during an SPF check 15 | type Resolver interface { 16 | Resolve(ctx context.Context, r *dns.Msg) (*dns.Msg, error) 17 | } 18 | 19 | var _ Resolver = &DefaultResolver{} 20 | 21 | // DefaultResolver is the Resolver that will be used in default constructed Checkers. 22 | type DefaultResolver struct { 23 | client *dns.Client 24 | servers []string 25 | } 26 | 27 | // Resolve performs a low level DNS lookup using miekg/dns format packet representation. 28 | func (res *DefaultResolver) Resolve(ctx context.Context, r *dns.Msg) (*dns.Msg, error) { 29 | if res.client == nil { 30 | clientConfig, err := dns.ClientConfigFromFile(ResolvConf) 31 | if err != nil { 32 | return nil, fmt.Errorf("Failed to load %s: %w", ResolvConf, err) 33 | } 34 | if len(clientConfig.Servers) == 0 { 35 | return nil, fmt.Errorf("No nameservers configured in %s", ResolvConf) 36 | } 37 | res.servers = make([]string, len(clientConfig.Servers)) 38 | for i, server := range clientConfig.Servers { 39 | res.servers[i] = fmt.Sprintf("%s:%s", server, clientConfig.Port) 40 | } 41 | res.client = new(dns.Client) 42 | } 43 | r.SetEdns0(4096, false) 44 | var m *dns.Msg 45 | var err error 46 | for _, server := range res.servers { 47 | m, _, err = res.client.ExchangeContext(ctx, r, server) 48 | if err == nil { 49 | return m, nil 50 | } 51 | } 52 | return m, err 53 | } 54 | -------------------------------------------------------------------------------- /resulttype_enumer.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type ResultType -transform=snake"; DO NOT EDIT. 2 | 3 | // 4 | package spf 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | const _ResultTypeName = "noneneutralpassfailsoftfailtemperrorpermerror" 11 | 12 | var _ResultTypeIndex = [...]uint8{0, 4, 11, 15, 19, 27, 36, 45} 13 | 14 | func (i ResultType) String() string { 15 | if i < 0 || i >= ResultType(len(_ResultTypeIndex)-1) { 16 | return fmt.Sprintf("ResultType(%d)", i) 17 | } 18 | return _ResultTypeName[_ResultTypeIndex[i]:_ResultTypeIndex[i+1]] 19 | } 20 | 21 | var _ResultTypeValues = []ResultType{0, 1, 2, 3, 4, 5, 6} 22 | 23 | var _ResultTypeNameToValueMap = map[string]ResultType{ 24 | _ResultTypeName[0:4]: 0, 25 | _ResultTypeName[4:11]: 1, 26 | _ResultTypeName[11:15]: 2, 27 | _ResultTypeName[15:19]: 3, 28 | _ResultTypeName[19:27]: 4, 29 | _ResultTypeName[27:36]: 5, 30 | _ResultTypeName[36:45]: 6, 31 | } 32 | 33 | // ResultTypeString retrieves an enum value from the enum constants string name. 34 | // Throws an error if the param is not part of the enum. 35 | func ResultTypeString(s string) (ResultType, error) { 36 | if val, ok := _ResultTypeNameToValueMap[s]; ok { 37 | return val, nil 38 | } 39 | return 0, fmt.Errorf("%s does not belong to ResultType values", s) 40 | } 41 | 42 | // ResultTypeValues returns all values of the enum 43 | func ResultTypeValues() []ResultType { 44 | return _ResultTypeValues 45 | } 46 | 47 | // IsAResultType returns "true" if the value is listed in the enum definition. "false" otherwise 48 | func (i ResultType) IsAResultType() bool { 49 | for _, v := range _ResultTypeValues { 50 | if i == v { 51 | return true 52 | } 53 | } 54 | return false 55 | } 56 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | //go:generate enumer -type ResultType -transform=snake 9 | 10 | // Result types, from RFC 7208 11 | // 2.6.1. None 12 | // 13 | // A result of "none" means either (a) no syntactically valid DNS domain 14 | // name was extracted from the SMTP session that could be used as the 15 | // one to be authorized, or (b) no SPF records were retrieved from 16 | // the DNS. 17 | // 18 | // 2.6.2. Neutral 19 | // 20 | // A "neutral" result means the ADMD has explicitly stated that it is 21 | // not asserting whether the IP address is authorized. 22 | // 23 | // 2.6.3. Pass 24 | // 25 | // A "pass" result is an explicit statement that the client is 26 | // authorized to inject mail with the given identity. 27 | // 28 | // 2.6.4. Fail 29 | // 30 | // A "fail" result is an explicit statement that the client is not 31 | // authorized to use the domain in the given identity. 32 | // 33 | // 2.6.5. Softfail 34 | // 35 | // A "softfail" result is a weak statement by the publishing ADMD that 36 | // the host is probably not authorized. It has not published a 37 | // stronger, more definitive policy that results in a "fail". 38 | // 39 | // 2.6.6. Temperror 40 | // 41 | // A "temperror" result means the SPF verifier encountered a transient 42 | // (generally DNS) error while performing the check. A later retry may 43 | // succeed without further DNS operator action. 44 | // 45 | // 2.6.7. Permerror 46 | // 47 | // A "permerror" result means the domain's published records could not 48 | // be correctly interpreted. This signals an error condition that 49 | // definitely requires DNS operator intervention to be resolved. 50 | 51 | // ResultType is the overall SPF result from checking a message. 52 | type ResultType int 53 | 54 | const ( 55 | None ResultType = iota 56 | Neutral 57 | Pass 58 | Fail 59 | Softfail 60 | Temperror 61 | Permerror 62 | ) 63 | 64 | // Result is all the information gathered during checking SPF for a message. 65 | type Result struct { 66 | Type ResultType 67 | Error error 68 | DNSQueries int 69 | VoidLookups int 70 | Explanation string 71 | UsedHelo bool 72 | ip net.IP 73 | sender string 74 | helo string 75 | c *Checker 76 | } 77 | 78 | func (r *Result) String() string { 79 | return r.Type.String() 80 | } 81 | 82 | // AuthenticationResults displays a Result as an RFC 8601 83 | // Authentication-Results: header 84 | func (r *Result) AuthenticationResults() string { 85 | if r.UsedHelo { 86 | return fmt.Sprintf("%s; spf=%s smtp.helo=%s", r.c.Hostname, r.Type.String(), r.helo) 87 | } 88 | return fmt.Sprintf("%s; spf=%s smtp.mailfrom=%s", r.c.Hostname, r.Type.String(), r.sender) 89 | } 90 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alvaroloes/enumer v1.1.2 h1:5khqHB33TZy1GWCO/lZwcroBFh7u+0j40T83VUbfAMY= 2 | github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= 3 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= 4 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 5 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 6 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 7 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 8 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 9 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 10 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 11 | github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU= 12 | github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ= 13 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 14 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 15 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 16 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 17 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 18 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 19 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 21 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 22 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 23 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 26 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 27 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 28 | golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 29 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 30 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 34 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://godoc.org/github.com/wttw/spf?status.svg)](https://godoc.org/github.com/wttw/spf) 2 | 3 | # A library to evaluate SPF policy records 4 | 5 | Complete, usable library to check whether a received email passes a 6 | published SPF (Sender Policy Framework) policy. 7 | 8 | It implements all of the SPF checker protocol as described in 9 | [RFC 7208](https://tools.wordtothewise.com/rfc7208), including macros and 10 | PTR checks, and passes 100% of the openspf and pyspf test suites. 11 | 12 | A DNS stub resolver using [miekg/dns](https://github.com/miekg/dns) is 13 | included, but can be replaced by anything that implements the 14 | spf.Resolver interface. 15 | 16 | As well as providing an implementation of the SPF check_host() function it 17 | also provides hooks to instrument the checking process. The included example 18 | client uses these to show how an SPF record is evaluated. 19 | 20 | ## Use as a CLI tool 21 | 22 | ```shell 23 | spf is a commandline tool for evaluating spf records. 24 | 25 | spf -ip 8.8.8.8 -from steve@aol.com 26 | 27 | Result: softfail 28 | Error: 29 | Explanation: 30 | 31 | If run with the -trace flag it will show the steps take to check the spf 32 | record, and if the -dns flag is added it will show all the DNS queries 33 | involved. 34 | 35 | spf -help 36 | Usage of spf: 37 | -dns 38 | show dns queries 39 | -from string 40 | 821.From address 41 | -helo string 42 | domain used in 821.HELO 43 | -ip string 44 | ip address from which the message is sent 45 | -mechanisms 46 | show details about each mechanism 47 | -trace 48 | show evaluation of record 49 | ``` 50 | 51 | ```shell 52 | ./spf -trace -from n_e_i_bounces@insideapple.apple.com -ip 17.179.250.63 53 | insideapple.apple.com.: v=spf1 include:_spf-txn.apple.com include:_spf-mkt.apple.com include:_spf.apple.com ~all 54 | _spf-txn.apple.com.: v=spf1 ip4:17.151.1.0/24 ip4:17.171.37.0/24 ip4:17.111.110.0/23 ~all 55 | _spf-txn.apple.com. returns softfail: v=spf1 ip4:17.151.1.0/24 ip4:17.171.37.0/24 ip4:17.111.110.0/23 ~all 56 | insideapple.apple.com. included _spf-txn.apple.com which didn't match 57 | _spf-mkt.apple.com.: v=spf1 ip4:17.171.23.0/24 ip4:17.179.250.0/24 ip4:17.32.227.0/24 ip4:17.240.6.0/24 ip4:17.240.49.0/24 ~all 58 | _spf-mkt.apple.com. returns pass: v=spf1 ip4:17.171.23.0/24 ip4:17.179.250.0/24 ip4:17.32.227.0/24 ip4:17.240.6.0/24 ip4:17.240.49.0/24 ~all 59 | insideapple.apple.com. included _spf-mkt.apple.com which matched, so the include returned pass 60 | insideapple.apple.com. returns pass: v=spf1 include:_spf-txn.apple.com include:_spf-mkt.apple.com include:_spf.apple.com ~all 61 | Result: pass 62 | Error: 63 | Explanation: 64 | ``` 65 | 66 | ### Installing binaries 67 | 68 | Binary releases of the commandline tool `spf` are available under [Releases](https://github.com/wttw/spf/releases). 69 | 70 | You'll need to unpack them with `tar zxf spf-.tar.gz` or unzip the Windows packages. 71 | 72 | These are built automatically and right now the workflow doesn't sign the binaries. You'll need to bypass 73 | the check for that, e.g. on macOS open it in finder, right click on it and select `Open` then give permission 74 | for it to run. 75 | 76 | ## Use as a library 77 | 78 | ```go 79 | import "github.com/wttw/spf" 80 | 81 | ip := net.ParseIP("8.8.8.8") 82 | result, _ := spf.Check(context.Background(), ip, "steve@aol.com", "aol.com") 83 | fmt.Println(result) 84 | ``` 85 | -------------------------------------------------------------------------------- /testdata/openspf/pyspf-tests.yml: -------------------------------------------------------------------------------- 1 | # PySPF test suite 2 | # $Id$ 3 | # vim:sw=2 sts=2 4 | # 5 | # This is the test suite used during development of the pyspf library. 6 | # It is a collection of ad hoc tests based on bug reports. It is the 7 | # goal of the SPF test project to have an elegant and minimal test suite 8 | # that reflects RFC 4408. However, this should help get things started 9 | # by serving as a example of what tests look like. Also, any implementation 10 | # that flunks this, should flunk the minimal elegant suite as well. 11 | --- 12 | description: Check basic exists with macros 13 | tests: 14 | exists-pass: 15 | helo: mail.example.net 16 | host: 1.2.3.5 17 | mailfrom: lyme.eater@example.co.uk 18 | result: pass 19 | exists-fail: 20 | helo: mail.example.net 21 | host: 1.2.3.4 22 | mailfrom: lyme.eater@example.co.uk 23 | result: fail 24 | zonedata: 25 | lyme.eater.example.co.uk.1.2.3.5.spf.example.net: 26 | - A: 127.0.0.1 27 | example.co.uk: 28 | - SPF: v=spf1 mx/26 exists:%{l}.%{d}.%{i}.spf.example.net -all 29 | --- 30 | description: Permerror detection 31 | tests: 32 | incloop: 33 | description: Include loop 34 | helo: mail.example.com 35 | host: 66.150.186.79 36 | mailfrom: chuckvsr@examplea.com 37 | result: permerror 38 | badall: 39 | helo: mail.example.com 40 | host: 66.150.186.79 41 | mailfrom: chuckvsr@examplec.com 42 | result: permerror 43 | baddomain: 44 | helo: mail.example.com 45 | host: 66.150.186.79 46 | mailfrom: chuckvsr@exampled.com 47 | result: permerror 48 | badip: 49 | helo: mail.example.com 50 | host: 66.150.186.79 51 | mailfrom: chuckvsr@examplee.com 52 | result: permerror 53 | zonedata: 54 | examplea.com: 55 | - SPF: v=spf1 a mx include:b.com 56 | exampleb.com: 57 | - SPF: v=spf1 a mx include:a.com 58 | examplec.com: 59 | - SPF: v=spf1 -all:foobar 60 | exampled.com: 61 | - SPF: v=spf1 a:examplea.com:8080 62 | examplee.com: 63 | - SPF: v=spf1 ip4:1.2.3.4:8080 64 | --- 65 | tests: 66 | nospace1: 67 | description: Test no space, test multi-line comment 68 | helo: mail.example1.com 69 | host: 1.2.3.4 70 | mailfrom: foo@example2.com 71 | result: none 72 | empty: 73 | description: Test empty 74 | helo: mail1.example1.com 75 | host: 1.2.3.4 76 | mailfrom: foo@example1.com 77 | result: neutral 78 | nospace2: 79 | helo: mail.example1.com 80 | host: 1.2.3.4 81 | mailfrom: foo@example3.com 82 | result: pass 83 | zonedata: 84 | example3.com: 85 | - SPF: v=spf1mx 86 | - SPF: v=spf1 mx 87 | - MX: [0, mail.example1.com] 88 | example1.com: 89 | - SPF: v=spf1 90 | example2.com: 91 | - SPF: v=spf1mx 92 | mail.example1.com: 93 | - A: 1.2.3.4 94 | --- 95 | description: Check trailing dot with redirect and exp 96 | tests: 97 | traildot1: 98 | spec: 8.1 99 | description: Trailing dot must be accepted for domains. 100 | helo: msgbas2x.cos.example.com 101 | host: 192.168.218.40 102 | mailfrom: test@example.com 103 | result: pass 104 | traildot2: 105 | spec: 8.1 106 | description: Trailing dot must not be removed from explanation. 107 | helo: msgbas2x.cos.example.com 108 | host: 192.168.218.40 109 | mailfrom: test@exp.example.com 110 | result: fail 111 | explanation: This is a test. 112 | zonedata: 113 | example.com.d.spf.example.com: 114 | - SPF: v=spf1 redirect=a.spf.example.com 115 | a.spf.example.com: 116 | - SPF: >- 117 | v=spf1 mx:example.com include:o.spf.example.com -exists:%{s}.S.bl.spf.example.com 118 | exists:%{s}.S.%{i}.AI.spf.example.com ~all 119 | o.spf.example.com: 120 | - SPF: v=spf1 ip4:192.168.144.41 ip4:192.168.218.40 ip4:192.168.218.41 121 | msgbas1x.cos.example.com: 122 | - A: 192.168.240.36 123 | example.com: 124 | - A: 192.168.90.76 125 | - SPF: v=spf1 redirect=%{d}.d.spf.example.com. 126 | - MX: [10, msgbas1x.cos.example.com] 127 | exp.example.com: 128 | - SPF: v=spf1 exp=msg.example.com. -all 129 | msg.example.com: 130 | - TXT: This is a test. 131 | --- 132 | description: Corner cases 133 | tests: 134 | emptyMX: 135 | description: Test empty MX 136 | helo: mail.example.com 137 | host: 1.2.3.4 138 | mailfrom: "" 139 | result: neutral 140 | localhost: 141 | helo: mail.example.com 142 | host: 127.0.0.1 143 | mailfrom: root@example.com 144 | result: fail 145 | zonedata: 146 | mail.example.com: 147 | - MX: [0, ""] 148 | - SPF: v=spf1 mx 149 | example.com: 150 | - SPF: v=spf1 -all -------------------------------------------------------------------------------- /ptr.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/miekg/dns" 7 | "strings" 8 | ) 9 | 10 | // 5.5. "ptr" (do not use) (RFC 7208) 11 | // 12 | // This mechanism tests whether the DNS reverse-mapping for exists 13 | // and correctly points to a domain name within a particular domain. 14 | // This mechanism SHOULD NOT be published. See the note at the end of 15 | // this section for more information. 16 | // 17 | // ptr = "ptr" [ ":" domain-spec ] 18 | // 19 | // The 's name is looked up using this procedure: 20 | // 21 | // o Perform a DNS reverse-mapping for : Look up the corresponding 22 | // PTR record in "in-addr.arpa." if the address is an IPv4 address 23 | // and in "ip6.arpa." if it is an IPv6 address. 24 | // 25 | // o For each record returned, validate the domain name by looking up 26 | // its IP addresses. To prevent DoS attacks, the PTR processing 27 | // limits defined in Section 4.6.4 MUST be applied. If they are 28 | // exceeded, processing is terminated and the mechanism does not 29 | // match. 30 | // 31 | // o If is among the returned IP addresses, then that domain name 32 | // is validated. 33 | // 34 | // Check all validated domain names to see if they either match the 35 | // domain or are a subdomain of the domain. 36 | // If any do, this mechanism matches. If no validated domain name can 37 | // be found, or if none of the validated domain names match or are a 38 | // subdomain of the , this mechanism fails to match. If a 39 | // DNS error occurs while doing the PTR RR lookup, then this mechanism 40 | // fails to match. If a DNS error occurs while doing an A RR lookup, 41 | // then that domain name is skipped and the search continues. 42 | // 43 | // This mechanism matches if 44 | // 45 | // o the is a subdomain of a validated domain name, or 46 | // 47 | // o the and a validated domain name are the same. 48 | // 49 | // For example, "mail.example.com" is within the domain "example.com", 50 | // but "mail.bad-example.com" is not. 51 | 52 | // MechanismPTR represents the SPF "ptr" mechanism. 53 | func (m MechanismPTR) Evaluate(ctx context.Context, result *Result, domain string) (ResultType, error) { 54 | c := result.c 55 | var qtype uint16 56 | if result.ip.To4() != nil { 57 | qtype = dns.TypeA 58 | } else { 59 | qtype = dns.TypeAAAA 60 | } 61 | 62 | target, err := result.c.ExpandDomainSpec(ctx, m.DomainSpec, result, domain, false) 63 | if err != nil { 64 | return Permerror, err 65 | } 66 | target = dns.Fqdn(target) 67 | if !validDomainName(target) { 68 | return Fail, fmt.Errorf("invalid hostname '%s'", target) 69 | } 70 | 71 | rev, err := dns.ReverseAddr(result.ip.String()) 72 | if err != nil { 73 | return Permerror, err 74 | } 75 | rrs, resultType, err := c.lookupDNS(ctx, rev, dns.TypePTR, result) 76 | if err != nil { 77 | return resultType, err 78 | } 79 | 80 | // When evaluating the "ptr" mechanism or the %{p} macro, the number of 81 | // "PTR" resource records queried is included in the overall limit of 10 82 | // mechanisms/modifiers that cause DNS lookups as described above. In 83 | // addition to that limit, the evaluation of each "PTR" record MUST NOT 84 | // result in querying more than 10 address records -- either "A" or 85 | // "AAAA" resource records. If this limit is exceeded, all records 86 | // other than the first 10 MUST be ignored. 87 | 88 | if len(rrs) > c.PtrAddressLimit { 89 | rrs = rrs[:c.PtrAddressLimit] 90 | } 91 | 92 | for _, rr := range rrs { 93 | hostname := rr.(*dns.PTR).Ptr 94 | // If it's never going to match, skip the A/AAAA lookups 95 | if !dns.IsSubDomain(target, hostname) { 96 | continue 97 | } 98 | 99 | addresses, _, err := c.lookupAddresses(ctx, hostname, qtype, result) 100 | if err != nil { 101 | continue 102 | } 103 | 104 | for _, address := range addresses { 105 | if address.Equal(result.ip) { 106 | // this hostname is validated and matches 107 | return m.Qualifier, nil 108 | } 109 | } 110 | } 111 | return None, nil 112 | } 113 | 114 | func expandPtrMacro(ctx context.Context, result *Result, target string) string { 115 | c := result.c 116 | var qtype uint16 117 | if result.ip.To4() != nil { 118 | qtype = dns.TypeA 119 | } else { 120 | qtype = dns.TypeAAAA 121 | } 122 | rev, err := dns.ReverseAddr(result.ip.String()) 123 | if err != nil { 124 | return "unknown" 125 | } 126 | rrs, _, err := c.lookupDNS(ctx, rev, dns.TypePTR, result) 127 | if err != nil { 128 | return "unknown" 129 | } 130 | if len(rrs) > c.PtrAddressLimit { 131 | rrs = rrs[:c.PtrAddressLimit] 132 | } 133 | 134 | possibles := []string{} 135 | target = dns.Fqdn(target) 136 | for _, rr := range rrs { 137 | hostname := rr.(*dns.PTR).Ptr 138 | addresses, _, err := c.lookupAddresses(ctx, hostname, qtype, result) 139 | if err != nil { 140 | continue 141 | } 142 | 143 | for _, address := range addresses { 144 | if address.Equal(result.ip) { 145 | // this hostname is validated and matches 146 | if strings.ToLower(hostname) == strings.ToLower(target) { 147 | return strings.TrimSuffix(hostname, ".") 148 | } 149 | possibles = append(possibles, hostname) 150 | break; 151 | } 152 | } 153 | } 154 | for _, possible := range possibles { 155 | if dns.IsSubDomain(target, possible) { 156 | return strings.TrimSuffix(possible, ".") 157 | } 158 | } 159 | if len(possibles) > 0 { 160 | return strings.TrimSuffix(possibles[0], ".") 161 | } 162 | return "unknown" 163 | } 164 | -------------------------------------------------------------------------------- /testdata/openspf/rfc4408-tests-CHANGES: -------------------------------------------------------------------------------- 1 | # Legend: 2 | # --- = A new release 3 | # ! = Added a test case or otherwise tightened a requirement, possibly 4 | # causing implementations to become incompliant with the current 5 | # test-suite release 6 | # - = Removed a test case or otherwise relaxed a requirement 7 | # * = Fixed a bug, or made a minor improvement 8 | -- UNRELEASED 9 | ! Added new test 'two-spaces' to test for proper treatment of records with 10 | more than one space between terms. 11 | --- 2009.10 (2009-10-31 20:00) 12 | ! Added test case: 13 | ! "macro-multiple-delimiters": 14 | Multiple delimiters in a macro expression must be supported. 15 | * Fixed "multitxt2" test case failing with SPF-type-only implementations. 16 | Tolerate a "None" result to accomodate those. 17 | --- 2008.08 (2008-08-17 16:00) 18 | ! "invalid-domain-empty-label", "invalid-domain-long", 19 | "invalid-domain-long-via-macro" test cases: 20 | A that is a valid domain-spec per RFC 4408 but an invalid 21 | domain name per RFC 1035 (two successive dots or labels longer than 63 22 | characters) must be treated either as a "PermError" or as non-existent and 23 | thus a no-match. (In particular, those cases can never cause a TempError 24 | because the error is guaranteed to reoccur given the same input data. 25 | This applies likewise to RFC-1035-invalid s that are the 26 | result of macro expansion.) Refined descriptions and comments to that 27 | end. 28 | The no-match behavior can be inferred by analogy from 4.3/1 and 5/10/3. 29 | The spec reference to 8.1/2 is bogus because the formal grammar does not 30 | preclude such invalid domain names. 31 | ! The "exp= without domain-spec" controversy has been resolved; it must be a 32 | syntax error. Tightened "exp-empty-domain" test case accordingly. 33 | ! Added test cases: 34 | ! "a-dash-in-toplabel": 35 | may contain dashes. Implementations matching 36 | non-greedily may get that wrong. 37 | ! "a-only-toplabel", "a-only-toplabel-trailing-dot": 38 | Both "a:museum" and "a:museum." are invalid syntax. A bare top-label is 39 | insufficient, with or without a trailing dot. 40 | ! "exp-no-txt", "exp-dns-error": 41 | Clearly, "exp=" referring to a non-existent TXT RR, or the look-up 42 | resulting in a DNS error, must cause the "exp=" modifier to be ignored per 43 | 6.2/4. 44 | ! "macro-mania-in-domain": 45 | Test macro-encoded percents (%%), spaces (%_), and URL-percent-encoded 46 | spaces (%20) in . 47 | ! "macro-reverse-split-on-dash": 48 | Test transformation of macro expansion results: splitting on non-dot 49 | separator characters, reversal, number of right-hand parts to use. 50 | - Removed "a-valid-syntax-but-unqueryable" test case. It is redundant to 51 | the "invalid-domain-empty-label" test case. 52 | - Relaxed "multispf1" test case: 53 | If performed via live DNS (yes, some people do that!), this test may be 54 | ineffective as DNS resolvers may combine multiple identical RRs. Thus, 55 | tolerate the test failing in this manner. 56 | * Adjusted "multispf2" test case: 57 | Avoid combination of multiple identical RRs by using different 58 | capitalization in intentionally duplicate RRs. 59 | * Renamed test cases: 60 | a-numeric-top-label -> a-numeric-toplabel 61 | a-bad-toplab -> a-bad-toplabel 62 | --- 2007.05 (2007-05-30 21:00) 63 | - "exp-empty-domain" test case is subject to controversy. "exp=" with an 64 | empty domain-spec may be considered a syntax error or not, thus both "Fail" 65 | and "PermError" results are acceptable for now. 66 | * Renamed the old "exp-syntax-error" test case to "explanation-syntax-error" 67 | to indicate that it refers to syntax errors in the explanation string, not 68 | in the "exp=" modifier. 69 | ! Added test cases: 70 | ! "exp-syntax-error", "redirect-syntax-error": Syntax errors in "exp=" and 71 | "redirect=" must be treated as such. 72 | ! "a-empty-domain", "mx-empty-domain", "ptr-empty-domain", 73 | "include-empty-domain", "redirect-empty-domain": "a:", "mx:", "ptr:", 74 | "include:", and "redirect=" with an empty domain-spec are syntax errors. 75 | ! "include-cidr": "include:/" is a syntax error. 76 | ! "helo-not-fqdn", "helo-domain-literal", "domain-literal": A non-FQDN 77 | HELO or MAIL FROM must result in a "None" result. 78 | ! "hello-domain-literal": Macro expansion results must not be checked for 79 | syntax errors, but must rather be treated as non-matches if nonsensical. 80 | ! "false-a-limit": There is no limit for the number of A records resulting 81 | from an "a:"-induced lookup, and no such limit must be imposed. 82 | ! "default-modifier-obsolete(2)": The "default=" modifier used in very old 83 | spec drafts must be ignored by RFC 4408 implementations. 84 | --- 2007.01 (2007-01-14 05:19) 85 | ! Added test cases: 86 | ! "nospftxttimeout": If no SPF-type record is present and the TXT lookup 87 | times out, the result must either be "None" (preferred) or "TempError". 88 | ! "exp-multiple-txt", "exp-syntax-error": Multiple explanation string TXT 89 | records and syntax errors in explanation strings must be ignored (i.e., 90 | specifically "PermError" must NOT be returned). 91 | ! "exp-empty-domain": "exp=" with an empty domain-spec is to be tolerated, 92 | i.e., ignored, too. (This is under debate.) 93 | ! "exp-twice", "redirect-twice": Added. Multiple "exp=" or "redirect=" 94 | modifiers are prohibited. 95 | * "Macro expansion rules" scenario: Fixed a bug that caused TXT-only 96 | implementations to fail several tests incorrectly due to a real TXT record 97 | blocking the automatic synthesis of TXT records from the corresponding 98 | SPF-type records. 99 | --- 2006.11 (initial release) (2006-11-27 21:27) 100 | # $Id$ 101 | # vim:tw=79 sts=2 sw=2 -------------------------------------------------------------------------------- /dns_helpers.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/miekg/dns" 7 | "net" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | var spfPrefixRe = regexp.MustCompile(`(?i)^v=spf1(?: |$)`) 14 | 15 | // Gets a single SPF record for a domain, as a single string 16 | func (c *Checker) getSPFRecord(ctx context.Context, domain string) (string, ResultType, error) { 17 | r := &dns.Msg{} 18 | r.SetQuestion(dns.Fqdn(domain), dns.TypeTXT) 19 | m, err := c.resolve(ctx, r) 20 | if err != nil { 21 | return "", Temperror, err 22 | } 23 | // 4.4. Record Lookup (RFC 7208) 24 | // If the DNS lookup returns a server failure (RCODE 2) or some other 25 | // error (RCODE other than 0 or 3), or if the lookup times out, then 26 | // check_host() terminates immediately with the result "temperror". 27 | switch m.Rcode { 28 | case dns.RcodeSuccess, dns.RcodeNameError: 29 | default: 30 | return "", Temperror, nil 31 | } 32 | 33 | // 4.5. Selecting Records (RFC 7208) 34 | // 35 | // Records begin with a version section: 36 | // 37 | // record = version terms *SP 38 | // version = "v=spf1" 39 | // 40 | // Starting with the set of records that were returned by the lookup, 41 | // discard records that do not begin with a version section of exactly 42 | // "v=spf1". Note that the version section is terminated by either an 43 | // SP character or the end of the record. As an example, a record with 44 | // a version section of "v=spf10" does not match and is discarded. 45 | 46 | spfRecords := make([]string, 0, 1) 47 | for _, rr := range m.Answer { 48 | txt, ok := rr.(*dns.TXT) 49 | if !ok { 50 | continue 51 | } 52 | record := strings.Join(txt.Txt, "") 53 | if spfPrefixRe.MatchString(record) { 54 | spfRecords = append(spfRecords, record) 55 | } 56 | } 57 | 58 | // 4.5. Selecting Records (RFC 7208) 59 | // 60 | // If the resultant record set includes no records, check_host() 61 | // produces the "none" result. If the resultant record set includes 62 | // more than one record, check_host() produces the "permerror" result. 63 | 64 | switch len(spfRecords) { 65 | case 0: 66 | return "", None, nil 67 | case 1: 68 | return spfRecords[0], None, nil 69 | default: 70 | return "", Permerror, nil 71 | } 72 | } 73 | 74 | var validDomainSuffix = regexp.MustCompile(`(?i)\.([a-z0-9][a-z0-9-]*[a-z0-9])\.?$`) 75 | var allNumeric = regexp.MustCompile(`^[0-9]*$`) 76 | 77 | // DNS allows arbitrary 8 bit data, so a simple dns.IsDomainName() isn't strict enough 78 | func validDomainName(hostname string) bool { 79 | atoms, ok := dns.IsDomainName(hostname) 80 | if !ok || atoms < 2 { 81 | return false 82 | } 83 | //if domainInvalidChars.MatchString(hostname) { 84 | // return false 85 | //} 86 | 87 | matches := validDomainSuffix.FindStringSubmatch(hostname) 88 | if matches == nil { 89 | return false 90 | } 91 | if allNumeric.MatchString(matches[1]) { 92 | return false 93 | } 94 | return true 95 | } 96 | 97 | func validOptionalDomainSpec(domainSpec string) bool { 98 | return domainSpec == "" || validDomainSpec(domainSpec) 99 | } 100 | 101 | // 7.1. Formal Specification 102 | // 103 | // The ABNF description for a macro is as follows: 104 | // 105 | // domain-spec = macro-string domain-end 106 | // domain-end = ( "." toplabel [ "." ] ) / macro-expand 107 | // 108 | // toplabel = ( *alphanum ALPHA *alphanum ) / 109 | // ( 1*alphanum "-" *( alphanum / "-" ) alphanum ) 110 | // alphanum = ALPHA / DIGIT 111 | // 112 | 113 | // .. so the domainSpec must end in either a macro token or a TLD 114 | 115 | func validDomainSpec(domainSpec string) bool { 116 | if validDomainName(domainSpec) { 117 | return true 118 | } 119 | if !MacroIsValid(domainSpec) { 120 | return false 121 | } 122 | if strings.HasSuffix(domainSpec, "}") { 123 | return true 124 | } 125 | matches := validDomainSuffix.FindStringSubmatch(domainSpec) 126 | if matches == nil { 127 | return false 128 | } 129 | if allNumeric.MatchString(matches[1]) { 130 | return false 131 | } 132 | return true 133 | } 134 | 135 | // lookupDNS does a basic DNS query, returning only matching records, and deals 136 | // with void query lookups 137 | func (c *Checker) lookupDNS(ctx context.Context, hostname string, qtype uint16, result *Result) ([]dns.RR, ResultType, error) { 138 | r := &dns.Msg{} 139 | r.SetQuestion(dns.Fqdn(hostname), qtype) 140 | m, err := c.resolve(ctx, r) 141 | if err != nil { 142 | return []dns.RR{}, Temperror, err 143 | } 144 | 145 | if m.Rcode == dns.RcodeNameError || (m.Rcode == dns.RcodeSuccess && len(m.Answer) == 0) { 146 | // NXDOMAIN or zero records 147 | result.VoidLookups++ 148 | if result.VoidLookups > c.VoidQueryLimit { 149 | return []dns.RR{}, Permerror, fmt.Errorf("void queries exceeded limit of %d", c.VoidQueryLimit) 150 | } 151 | return []dns.RR{}, None, nil 152 | } 153 | 154 | if m.Rcode != dns.RcodeSuccess { 155 | return []dns.RR{}, Temperror, nil 156 | } 157 | 158 | ret := make([]dns.RR, 0, len(m.Answer)) 159 | for _, rr := range m.Answer { 160 | if rr.Header().Rrtype == qtype { 161 | ret = append(ret, rr) 162 | } 163 | } 164 | return ret, None, nil 165 | } 166 | 167 | // lookupAddresses does either an A or AAAA lookup, returning matching results as []net.IP 168 | func (c *Checker) lookupAddresses(ctx context.Context, target string, qtype uint16, result *Result) ([]net.IP, ResultType, error) { 169 | ret := []net.IP{} 170 | rrs, resultType, err := c.lookupDNS(ctx, target, qtype, result) 171 | if resultType != None { 172 | return []net.IP{}, resultType, err 173 | } 174 | for _, rr := range rrs { 175 | switch v := rr.(type) { 176 | case *dns.A: 177 | ret = append(ret, v.A) 178 | case *dns.AAAA: 179 | ret = append(ret, v.AAAA) 180 | } 181 | } 182 | return ret, None, nil 183 | } 184 | 185 | // like net.ParseCIDR but a little less forgiving 186 | func parseCIDR(s string) (net.IP, *net.IPNet, error) { 187 | ip, mask, err := net.ParseCIDR(s) 188 | if err != nil { 189 | return nil, nil, err 190 | } 191 | i := strings.Index(s, "/") 192 | if i < 0 { 193 | return nil, nil, &net.ParseError{Type: "CIDR address", Text: s} 194 | } 195 | 196 | maskIn := s[i+1:] 197 | ones, _ := mask.Mask.Size() 198 | if maskIn != strconv.Itoa(ones) { 199 | return nil, nil, &net.ParseError{Type: "CIDR address", Text: s} 200 | } 201 | return ip, mask, err 202 | } 203 | -------------------------------------------------------------------------------- /testdata/openspf/schema.rng: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 1 95 | 2 96 | 3 97 | 4 98 | 5 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /cmd/spf/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | spf is a commandline tool for evaluating spf records. 3 | 4 | spf -ip 8.8.8.8 -from steve@aol.com 5 | 6 | Result: softfail 7 | Error: 8 | Explanation: 9 | 10 | If run with the -trace flag it will show the steps take to check the spf 11 | record, and if the -dns flag is added it will show all the DNS queries 12 | involved. 13 | 14 | spf -help 15 | Usage of spf: 16 | -dns 17 | show dns queries 18 | -from string 19 | 821.From address 20 | -helo string 21 | domain used in 821.HELO 22 | -ip string 23 | ip address from which the message is sent 24 | -mechanisms 25 | show details about each mechanism 26 | -trace 27 | show evaluation of record 28 | */ 29 | package main 30 | 31 | import ( 32 | "context" 33 | "flag" 34 | "fmt" 35 | "github.com/logrusorgru/aurora" 36 | "github.com/mattn/go-colorable" 37 | "github.com/mattn/go-isatty" 38 | "github.com/miekg/dns" 39 | "io" 40 | "log" 41 | "net" 42 | "os" 43 | "regexp" 44 | "strings" 45 | 46 | "github.com/wttw/spf" 47 | ) 48 | 49 | 50 | 51 | func main() { 52 | var ip, from, domain, helo string 53 | var trace, showDns, mechanisms bool 54 | flag.StringVar(&ip, "ip", "", "ip address from which the message is sent") 55 | flag.StringVar(&from, "from", "", "821.From address") 56 | flag.StringVar(&helo, "helo", "", "domain used in 821.HELO") 57 | flag.BoolVar(&trace, "trace", false, "show evaluation of record") 58 | flag.BoolVar(&showDns, "dns", false, "show dns queries") 59 | flag.BoolVar(&mechanisms, "mechanisms", false, "show details about each mechanism") 60 | flag.Parse() 61 | 62 | if ip == "" { 63 | log.Fatalln("-ip is required") 64 | } 65 | 66 | if from == "" { 67 | log.Fatalln("-from is required") 68 | } 69 | 70 | if domain == "" { 71 | at := strings.LastIndex(from, "@") 72 | domain = from[at+1:] 73 | } 74 | 75 | addr := net.ParseIP(ip) 76 | if addr == nil { 77 | log.Fatalf("'%s' doesn't look like an ip address", ip) 78 | } 79 | 80 | c := spf.NewChecker() 81 | if trace { 82 | au := aurora.NewAurora(isatty.IsTerminal(os.Stdout.Fd())) 83 | stdout := colorable.NewColorableStdout() 84 | c.Hook = &Tracer{ 85 | au: au, 86 | stdout: stdout, 87 | dns: showDns, 88 | showMechanisms: mechanisms, 89 | records: map[string]spfMechanismResults{}, 90 | } 91 | } 92 | ctx := context.Background() 93 | result := c.SPF(ctx, addr, from, helo) 94 | fmt.Printf("Result: %v\nError: %v\nExplanation: %s\n", result.Type, result.Error, result.Explanation) 95 | } 96 | 97 | type spfMechanismResult struct { 98 | result spf.ResultType 99 | mechanism spf.Mechanism 100 | } 101 | 102 | type spfMechanismResults struct { 103 | record string 104 | results map[int]spfMechanismResult 105 | associatedRecords []string 106 | } 107 | 108 | type Tracer struct { 109 | au aurora.Aurora 110 | stdout io.Writer 111 | dns bool 112 | showMechanisms bool 113 | lastMechanismDomain string 114 | records map[string]spfMechanismResults 115 | depth int 116 | } 117 | 118 | func (t *Tracer) resultColour(resultType spf.ResultType, msg string) aurora.Value { 119 | switch resultType { 120 | case spf.Temperror, spf.Permerror: 121 | return t.au.BrightRed(msg) 122 | case spf.None, spf.Neutral: 123 | return t.au.Blue(msg) 124 | case spf.Fail, spf.Softfail: 125 | return t.au.Red(msg) 126 | case spf.Pass: 127 | return t.au.Green(msg) 128 | } 129 | return t.au.BrightRed(fmt.Sprintf("unknown result type %v", resultType)) 130 | } 131 | 132 | func (t *Tracer) resultString(resultType spf.ResultType) aurora.Value { 133 | return t.resultColour(resultType, resultType.String()) 134 | } 135 | 136 | func (t *Tracer) Printf(format string, a ...interface{}) (int, error) { 137 | return fmt.Fprintf(t.stdout, format, a...) 138 | } 139 | 140 | var _ spf.Hook = &Tracer{} 141 | 142 | func (t *Tracer) Dns(r *dns.Msg, m *dns.Msg, err error) { 143 | if t.dns { 144 | t.Printf("%s request for %s\n", dns.Type(r.Question[0].Qtype).String(), r.Question[0].Name) 145 | t.Printf("%s\n", t.au.Cyan(m.String())) 146 | } 147 | } 148 | 149 | func (t *Tracer) Macro(before, after string, err error) { 150 | if err == nil { 151 | if before != after { 152 | t.Printf("%s expands to %s\n", t.au.BgBlue(before), t.au.BgBlue(after)) 153 | } 154 | return 155 | } 156 | 157 | t.Printf("%s %s: %s\n", t.au.BgRed("Failed to expand macro"), t.au.BgBlue(before), t.au.Red(err.Error())) 158 | } 159 | 160 | func (t *Tracer) Record(record, domain string) { 161 | t.depth++ 162 | t.Printf("%s: %s\n", domain, t.au.Magenta(record)) 163 | t.lastMechanismDomain = "" 164 | t.records[domain] = spfMechanismResults{ 165 | record: record, 166 | results: map[int]spfMechanismResult{}, 167 | } 168 | } 169 | 170 | func (t *Tracer) Mechanism(domain string, index int, mechanism spf.Mechanism, result *spf.Result) { 171 | t.records[domain].results[index] = spfMechanismResult{ 172 | result: result.Type, 173 | mechanism: mechanism, 174 | } 175 | include, ok := mechanism.(spf.MechanismInclude) 176 | if ok { 177 | t.Printf("%s included %s", domain, include.DomainSpec) 178 | if result.Type == include.Qualifier { 179 | t.Printf(" which matched, so the include returned %s", t.resultString(result.Type)) 180 | } else { 181 | t.Printf(" which didn't match") 182 | } 183 | t.Printf("\n") 184 | } 185 | if t.showMechanisms { 186 | if t.lastMechanismDomain != domain { 187 | t.Printf("from %s\n", domain) 188 | t.lastMechanismDomain = domain 189 | } 190 | t.Printf(" %2d ", index+1) 191 | switch result.Type { 192 | case spf.Temperror, spf.Permerror: 193 | t.Printf("%s %s", mechanism.String(), t.resultString(result.Type)) 194 | case spf.None, spf.Neutral: 195 | t.Printf("%s (%s)", t.au.Blue(mechanism.String()), t.resultString(result.Type)) 196 | case spf.Fail, spf.Softfail: 197 | t.Printf("%s (%s)", mechanism.String(), t.resultString(result.Type)) 198 | case spf.Pass: 199 | t.Printf("%s (%s)", mechanism.String(), t.resultString(result.Type)) 200 | } 201 | if result.Error != nil { 202 | t.Printf(" (%s)", t.au.Red(result.Error.Error())) 203 | } 204 | 205 | t.Printf("\n") 206 | } 207 | } 208 | 209 | var modifierRe = regexp.MustCompile(`^((?i)[a-z][a-z0-9_.-]*)=(.*)`) 210 | 211 | func (t *Tracer) RecordResult(domain string, result *spf.Result) { 212 | t.depth-- 213 | t.Printf("%s returns %s: ", domain, t.resultString(result.Type)) 214 | spfRecord, ok := t.records[domain] 215 | if ok { 216 | fields := strings.Fields(spfRecord.record) 217 | i := 0 218 | for _, field := range fields { 219 | if modifierRe.MatchString(field) { 220 | t.Printf("%s ", field) 221 | } else { 222 | mech, ok := spfRecord.results[i] 223 | if !ok { 224 | t.Printf("%s ", t.au.Gray(15, field)) 225 | } else { 226 | t.Printf("%s ", t.resultColour(mech.result, field)) 227 | } 228 | i++ 229 | } 230 | } 231 | } 232 | t.Printf("\n") 233 | } 234 | 235 | func (t *Tracer) Redirect(target string) { 236 | t.Printf("redirecting to %s\n", target) 237 | } 238 | -------------------------------------------------------------------------------- /spf_test.go: -------------------------------------------------------------------------------- 1 | package spf_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/miekg/dns" 7 | "github.com/wttw/spf" 8 | "io" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "testing" 14 | 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | type Test struct { 19 | Spec interface{} 20 | Description string 21 | Helo string 22 | Host net.IP 23 | MailFrom string 24 | Result interface{} 25 | Explanation string 26 | } 27 | 28 | type Answer interface{} 29 | 30 | type Suite struct { 31 | Description string `yaml:"description"` 32 | Tests map[string]Test 33 | ZoneData map[string][]Answer 34 | } 35 | 36 | func (e Test) ResultMatches(s string) bool { 37 | acceptable := toSlice(e.Result) 38 | for _, a := range acceptable { 39 | if s == a { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | 46 | func toSlice(i interface{}) []string { 47 | switch v := i.(type) { 48 | case string: 49 | return []string{v} 50 | case []string: 51 | return v 52 | case []interface{}: 53 | ret := make([]string, len(v)) 54 | for j, k := range v { 55 | ret[j] = k.(string) 56 | } 57 | return ret 58 | default: 59 | panic(fmt.Errorf("unexpected type in RR: %T, %#v", i, i)) 60 | } 61 | } 62 | 63 | type TestResolver map[string]map[uint16]*dns.Msg 64 | 65 | var _ spf.Resolver = TestResolver{} 66 | 67 | func (res TestResolver) Resolve(_ context.Context, r *dns.Msg) (*dns.Msg, error) { 68 | m := &dns.Msg{} 69 | m.SetReply(r) 70 | hostRRs, ok := res[strings.ToLower(r.Question[0].Name)] 71 | if !ok { 72 | m.SetRcode(r, dns.RcodeNameError) // NXDOMAIN 73 | return m, nil 74 | } 75 | 76 | response, ok := hostRRs[r.Question[0].Qtype] 77 | if ok { 78 | m = response.Copy() 79 | m.SetReply(r) 80 | } else { 81 | 82 | _, ok = hostRRs[0] 83 | if ok { 84 | m.SetRcode(r, dns.RcodeServerFailure) // SERVFAIL 85 | return m, nil 86 | } 87 | } 88 | 89 | m.SetRcode(r, dns.RcodeSuccess) 90 | return m, nil 91 | } 92 | 93 | func (s Suite) Zone(t *testing.T) TestResolver { 94 | ret := TestResolver{} 95 | 96 | // Our test vectors have a weird mix of RRs in their sample DNS data 97 | // In some tests there are both SPF and TXT records, which should be used as-is 98 | // In others there's just SPF, which should all be treated as TXT (or duplicated 99 | // as TXT?) 100 | 101 | for hostname, answers := range s.ZoneData { 102 | hostname = strings.ToLower(dns.Fqdn(hostname)) 103 | _, ok := ret[hostname] 104 | if !ok { 105 | ret[hostname] = map[uint16]*dns.Msg{} 106 | } 107 | 108 | seenTXT := false 109 | for _, answer := range answers { 110 | switch v := answer.(type) { 111 | case map[interface{}]interface{}: 112 | for typeThing, _ := range v { 113 | typeString, ok := typeThing.(string) 114 | if ok && typeString == "TXT" /* && strings.HasPrefix(value.(string), "v=spf1")*/ { 115 | seenTXT = true 116 | } 117 | } 118 | } 119 | } 120 | 121 | for _, answer := range answers { 122 | switch v := answer.(type) { 123 | case string: 124 | if v != "TIMEOUT" { 125 | t.Fatalf("Unrecognized value '%s' in %s", v, hostname) 126 | } 127 | ret[hostname][0] = nil 128 | case map[interface{}]interface{}: 129 | for typeThing, value := range v { 130 | typeString, ok := typeThing.(string) 131 | if !ok { 132 | t.Fatalf("Unrecognized RR key %T in %s", typeThing, hostname) 133 | } 134 | typeID, ok := dns.StringToType[typeString] 135 | if !ok { 136 | t.Fatalf("Unrecognized RR type '%s' in %s", typeString, hostname) 137 | } 138 | 139 | var rr dns.RR 140 | hdr := dns.RR_Header{ 141 | Name: hostname, 142 | Rrtype: typeID, 143 | Class: dns.ClassINET, 144 | Ttl: 30, 145 | } 146 | switch typeID { 147 | case dns.TypeSPF: 148 | rr = &dns.SPF{ 149 | Hdr: hdr, 150 | Txt: toSlice(value), 151 | } 152 | case dns.TypeMX: 153 | slice := value.([]interface{}) 154 | weight := slice[0].(int) 155 | rr = &dns.MX{ 156 | Hdr: hdr, 157 | Preference: uint16(weight), 158 | Mx: dns.Fqdn(slice[1].(string)), 159 | } 160 | case dns.TypeTXT: 161 | rr = &dns.TXT{ 162 | Hdr: hdr, 163 | Txt: toSlice(value), 164 | } 165 | case dns.TypeA: 166 | rr = &dns.A{ 167 | Hdr: hdr, 168 | A: net.ParseIP(value.(string)), 169 | } 170 | case dns.TypeAAAA: 171 | rr = &dns.AAAA{ 172 | Hdr: hdr, 173 | AAAA: net.ParseIP(value.(string)), 174 | } 175 | case dns.TypePTR: 176 | rr = &dns.PTR{ 177 | Hdr: hdr, 178 | Ptr: dns.Fqdn(value.(string)), 179 | } 180 | case dns.TypeCNAME: 181 | rr = &dns.CNAME{ 182 | Hdr: hdr, 183 | Target: value.(string), 184 | } 185 | default: 186 | t.Fatalf("Unhandled RR type '%s' in %s", typeString, hostname) 187 | } 188 | 189 | if typeID == dns.TypeTXT && rr.(*dns.TXT).Txt[0] == "NONE" { 190 | continue 191 | } 192 | 193 | m, ok := ret[hostname][typeID] 194 | if !ok { 195 | m = &dns.Msg{} 196 | } 197 | m.Answer = append(m.Answer, rr) 198 | ret[hostname][typeID] = m 199 | 200 | // Dupe the SPF record to TXT 201 | if !seenTXT && typeID == dns.TypeSPF { 202 | m, ok := ret[hostname][dns.TypeTXT] 203 | if !ok { 204 | m = &dns.Msg{} 205 | } 206 | m.Answer = append(m.Answer, &dns.TXT{ 207 | Hdr: dns.RR_Header{ 208 | Name: hostname, 209 | Rrtype: dns.TypeTXT, 210 | Class: dns.ClassINET, 211 | Ttl: 30, 212 | }, 213 | Txt: toSlice(value), 214 | }) 215 | ret[hostname][dns.TypeTXT] = m 216 | } 217 | } 218 | default: 219 | t.Fatalf("Unexpected RR type %T, %#v in %s", answer, answer, hostname) 220 | } 221 | } 222 | } 223 | return ret 224 | } 225 | 226 | func loadSuites(t *testing.T, filename string) []Suite { 227 | suites := []Suite{} 228 | f, err := os.Open(filename) 229 | if err != nil { 230 | t.Fatalf("failed to open %s: %v", filename, err) 231 | } 232 | decoder := yaml.NewDecoder(f) 233 | for { 234 | var s Suite 235 | err = decoder.Decode(&s) 236 | if err != nil { 237 | if err == io.EOF { 238 | return suites 239 | } 240 | t.Fatalf("while reading %s: %v", filename, err) 241 | } 242 | suites = append(suites, s) 243 | } 244 | } 245 | 246 | func runSuite(s Suite) func(*testing.T) { 247 | return func(t *testing.T) { 248 | resolver := s.Zone(t) 249 | checker := spf.NewChecker() 250 | checker.Resolver = resolver 251 | for name, test := range s.Tests { 252 | t.Run(name, func(t *testing.T) { 253 | actual := checker.SPF(context.Background(), test.Host, test.MailFrom, test.Helo) 254 | if !test.ResultMatches(actual.String()) { 255 | t.Errorf("expected %v, actual %s", test.Result, actual.String()) 256 | } 257 | }) 258 | } 259 | } 260 | } 261 | 262 | func TestSPF(t *testing.T) { 263 | for _, filename := range []string{ 264 | "testdata/openspf/pyspf-tests.yml", 265 | "testdata/openspf/rfc7208-tests.yml", 266 | } { 267 | for _, s := range loadSuites(t, filename) { 268 | t.Run(filepath.Base(filename)+"/"+s.Description, runSuite(s)) 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /testdata/openspf/rfc7208-tests-CHANGES: -------------------------------------------------------------------------------- 1 | # Legend: 2 | # --- = A new release 3 | # ! = Added a test case or otherwise tightened a requirement, possibly 4 | # causing implementations to become incompliant with the current 5 | # test-suite release 6 | # - = Removed a test case or otherwise relaxed a requirement 7 | # * = Fixed a bug, or made a minor improvement 8 | -- UNRELEASED 9 | ! Added new test 'two-spaces' to test for proper treatment of records with 10 | more than one space between terms. 11 | ! Added new test 'trailing-space' to test that trailing spaces are ignored. 12 | ! Added new test 'ptr-case-change' to test inconsistent case from caching DNS 13 | servers, which has been common in the field recently. 14 | ! Added new test 'ptr-cname-loop' to test CNAME loops with inconsistent case 15 | in the name (so that a case sensitive compare with names already seen 16 | doesn't detect the loop). Some think the loop should generate a permerror 17 | instead of skipping the name, but I documented the spec language to justify 18 | skipping. 19 | --- 2014.05 (2014-05-16 05:00) 20 | ! Updates for RFC 7208 (4408bis) 21 | ! Updated multiple tests not to consider type SPF records under mixed 22 | conditions. Note that due to the way the test suite is structured, many 23 | records are still labled SPF internally, but for test functions, it 24 | doesn't matter externally. 25 | ! Modified multiple tests to remove ambiguous results for cases that were 26 | ambiguous in RFC 4408, but have been clarified in RFC 7208. 27 | ! Changed "mx-limit" test to produce permerror result per changes in RFC 28 | 7208. 29 | ! Added "void-over-limit", "void-at-limit", and "exp-void" tests for new 30 | void lookup limit. 31 | ! Added "invalid-trailing-macro-char" and "invalid-embedded-macro-char" 32 | tests from Stuart D. Gathman on pyspf trunk. 33 | --- 2009.10 (2009-10-31 20:00) 34 | ! Added test case: 35 | ! "macro-multiple-delimiters": 36 | Multiple delimiters in a macro expression must be supported. 37 | * Fixed "multitxt2" test case failing with SPF-type-only implementations. 38 | Tolerate a "None" result to accomodate those. 39 | --- 2008.08 (2008-08-17 16:00) 40 | ! "invalid-domain-empty-label", "invalid-domain-long", 41 | "invalid-domain-long-via-macro" test cases: 42 | A that is a valid domain-spec per RFC 4408 but an invalid 43 | domain name per RFC 1035 (two successive dots or labels longer than 63 44 | characters) must be treated either as a "PermError" or as non-existent and 45 | thus a no-match. (In particular, those cases can never cause a TempError 46 | because the error is guaranteed to reoccur given the same input data. 47 | This applies likewise to RFC-1035-invalid s that are the 48 | result of macro expansion.) Refined descriptions and comments to that 49 | end. 50 | The no-match behavior can be inferred by analogy from 4.3/1 and 5/10/3. 51 | The spec reference to 8.1/2 is bogus because the formal grammar does not 52 | preclude such invalid domain names. 53 | ! The "exp= without domain-spec" controversy has been resolved; it must be a 54 | syntax error. Tightened "exp-empty-domain" test case accordingly. 55 | ! Added test cases: 56 | ! "a-dash-in-toplabel": 57 | may contain dashes. Implementations matching 58 | non-greedily may get that wrong. 59 | ! "a-only-toplabel", "a-only-toplabel-trailing-dot": 60 | Both "a:museum" and "a:museum." are invalid syntax. A bare top-label is 61 | insufficient, with or without a trailing dot. 62 | ! "exp-no-txt", "exp-dns-error": 63 | Clearly, "exp=" referring to a non-existent TXT RR, or the look-up 64 | resulting in a DNS error, must cause the "exp=" modifier to be ignored per 65 | 6.2/4. 66 | ! "macro-mania-in-domain": 67 | Test macro-encoded percents (%%), spaces (%_), and URL-percent-encoded 68 | spaces (%20) in . 69 | ! "macro-reverse-split-on-dash": 70 | Test transformation of macro expansion results: splitting on non-dot 71 | separator characters, reversal, number of right-hand parts to use. 72 | - Removed "a-valid-syntax-but-unqueryable" test case. It is redundant to 73 | the "invalid-domain-empty-label" test case. 74 | - Relaxed "multispf1" test case: 75 | If performed via live DNS (yes, some people do that!), this test may be 76 | ineffective as DNS resolvers may combine multiple identical RRs. Thus, 77 | tolerate the test failing in this manner. 78 | * Adjusted "multispf2" test case: 79 | Avoid combination of multiple identical RRs by using different 80 | capitalization in intentionally duplicate RRs. 81 | * Renamed test cases: 82 | a-numeric-top-label -> a-numeric-toplabel 83 | a-bad-toplab -> a-bad-toplabel 84 | --- 2007.05 (2007-05-30 21:00) 85 | - "exp-empty-domain" test case is subject to controversy. "exp=" with an 86 | empty domain-spec may be considered a syntax error or not, thus both "Fail" 87 | and "PermError" results are acceptable for now. 88 | * Renamed the old "exp-syntax-error" test case to "explanation-syntax-error" 89 | to indicate that it refers to syntax errors in the explanation string, not 90 | in the "exp=" modifier. 91 | ! Added test cases: 92 | ! "exp-syntax-error", "redirect-syntax-error": Syntax errors in "exp=" and 93 | "redirect=" must be treated as such. 94 | ! "a-empty-domain", "mx-empty-domain", "ptr-empty-domain", 95 | "include-empty-domain", "redirect-empty-domain": "a:", "mx:", "ptr:", 96 | "include:", and "redirect=" with an empty domain-spec are syntax errors. 97 | ! "include-cidr": "include:/" is a syntax error. 98 | ! "helo-not-fqdn", "helo-domain-literal", "domain-literal": A non-FQDN 99 | HELO or MAIL FROM must result in a "None" result. 100 | ! "hello-domain-literal": Macro expansion results must not be checked for 101 | syntax errors, but must rather be treated as non-matches if nonsensical. 102 | ! "false-a-limit": There is no limit for the number of A records resulting 103 | from an "a:"-induced lookup, and no such limit must be imposed. 104 | ! "default-modifier-obsolete(2)": The "default=" modifier used in very old 105 | spec drafts must be ignored by RFC 4408 implementations. 106 | --- 2007.01 (2007-01-14 05:19) 107 | ! Added test cases: 108 | ! "nospftxttimeout": If no SPF-type record is present and the TXT lookup 109 | times out, the result must either be "None" (preferred) or "TempError". 110 | ! "exp-multiple-txt", "exp-syntax-error": Multiple explanation string TXT 111 | records and syntax errors in explanation strings must be ignored (i.e., 112 | specifically "PermError" must NOT be returned). 113 | ! "exp-empty-domain": "exp=" with an empty domain-spec is to be tolerated, 114 | i.e., ignored, too. (This is under debate.) 115 | ! "exp-twice", "redirect-twice": Added. Multiple "exp=" or "redirect=" 116 | modifiers are prohibited. 117 | * "Macro expansion rules" scenario: Fixed a bug that caused TXT-only 118 | implementations to fail several tests incorrectly due to a real TXT record 119 | blocking the automatic synthesis of TXT records from the corresponding 120 | SPF-type records. 121 | --- 2006.11 (initial release) (2006-11-27 21:27) 122 | # $Id$ 123 | # vim:tw=79 sts=2 sw=2 -------------------------------------------------------------------------------- /macro.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // 7. Macros (RFC 7208) 16 | // 17 | // When evaluating an SPF policy record, certain character sequences are 18 | // intended to be replaced by parameters of the message or of the 19 | // connection. These character sequences are referred to as "macros". 20 | // 21 | // 7.1. Formal Specification 22 | // 23 | // The ABNF description for a macro is as follows: 24 | // 25 | // domain-spec = macro-string domain-end 26 | // domain-end = ( "." toplabel [ "." ] ) / macro-expand 27 | // 28 | // toplabel = ( *alphanum ALPHA *alphanum ) / 29 | // ( 1*alphanum "-" *( alphanum / "-" ) alphanum ) 30 | // alphanum = ALPHA / DIGIT 31 | // 32 | // explain-string = *( macro-string / SP ) 33 | // 34 | // macro-string = *( macro-expand / macro-literal ) 35 | // macro-expand = ( "%{" macro-letter transformers *delimiter "}" ) 36 | // / "%%" / "%_" / "%-" 37 | // macro-literal = %x21-24 / %x26-7E 38 | // ; visible characters except "%" 39 | // macro-letter = "s" / "l" / "o" / "d" / "i" / "p" / "h" / 40 | // "c" / "r" / "t" / "v" 41 | // transformers = *DIGIT [ "r" ] 42 | // delimiter = "." / "-" / "+" / "," / "/" / "_" / "=" 43 | // 44 | // The "toplabel" construction is subject to the letter-digit-hyphen 45 | // (LDH) rule plus additional top-level domain (TLD) restrictions. See 46 | // Section 2 of [RFC3696] for background. 47 | // 48 | // Some special cases: 49 | // 50 | // o A literal "%" is expressed by "%%". 51 | // 52 | // o "%_" expands to a single " " space. 53 | // 54 | // o "%-" expands to a URL-encoded space, viz., "%20". 55 | // 56 | // 7.2. Macro Definitions 57 | // 58 | // The following macro letters are expanded in term arguments: 59 | // 60 | // s = 61 | // l = local-part of 62 | // o = domain of 63 | // d = 64 | // i = 65 | // p = the validated domain name of (do not use) 66 | // v = the string "in-addr" if is ipv4, or "ip6" if is ipv6 67 | // h = HELO/EHLO domain 68 | // 69 | // , , and are defined in Section 4.1. 70 | // 71 | // The following macro letters are allowed only in "exp" text: 72 | // 73 | // c = SMTP client IP (easily readable format) 74 | // r = domain name of host performing the check 75 | // t = current timestamp 76 | 77 | var macroRe = regexp.MustCompile(`^{([alodiphcrtvALODIPHCRTV])([0-9]{0,3})(r?)([.+=,/_-]*)}`) 78 | 79 | // MacroIsValid validates an SPF macro. 80 | func MacroIsValid(macroString string) bool { 81 | for { 82 | percent := strings.Index(macroString, "%") 83 | if percent == -1 { 84 | return true 85 | } 86 | macroString = macroString[percent+1:] 87 | if len(macroString) == 0 { 88 | return false 89 | } 90 | switch macroString[0] { 91 | case '%', '-', '_': 92 | macroString = macroString[1:] 93 | default: 94 | return false 95 | case '{': 96 | matches := macroRe.FindStringSubmatch(macroString) 97 | if matches == nil { 98 | return false 99 | } 100 | macroString = macroString[len(matches[0]):] 101 | } 102 | } 103 | } 104 | 105 | // ExpandMacro populates an SPF macro based on the current state of the check process. 106 | func (c *Checker) ExpandMacro(ctx context.Context, domainSpec string, result *Result, domain string, exp bool) (string, error) { 107 | expansion, err := c.expandMacro(ctx, domainSpec, result, domain, exp) 108 | if c.Hook != nil { 109 | c.Hook.Macro(domainSpec, expansion, err) 110 | } 111 | return expansion, err 112 | } 113 | 114 | func (c *Checker) expandMacro(ctx context.Context, domainSpec string, result *Result, domain string, exp bool) (string, error) { 115 | percent := strings.Index(domainSpec, "%") 116 | if percent == -1 { 117 | // short circuit common case 118 | return domainSpec, nil 119 | } 120 | var ret strings.Builder 121 | for { 122 | ret.WriteString(domainSpec[:percent]) 123 | domainSpec = domainSpec[percent+1:] 124 | if len(domainSpec) == 0 { 125 | return "", errors.New("trailing % in macro expansion") 126 | } 127 | switch domainSpec[0] { 128 | case '%': 129 | ret.WriteRune('%') 130 | domainSpec = domainSpec[1:] 131 | case '-': 132 | ret.WriteString("%20") 133 | domainSpec = domainSpec[1:] 134 | case '_': 135 | ret.WriteRune(' ') 136 | domainSpec = domainSpec[1:] 137 | default: 138 | return "", fmt.Errorf("invalid character '%c' following %% in macro expansion", domainSpec[0]) 139 | case '{': 140 | matches := macroRe.FindStringSubmatch(domainSpec) 141 | if len(matches) == 0 { 142 | return "", fmt.Errorf("invalid macro starting near %s", domainSpec) 143 | } 144 | macroLetter, macroLimit, macroReverse, macroDelimiters := matches[1], matches[2], matches[3], matches[4] 145 | domainSpec = domainSpec[len(matches[0]):] 146 | var replacement string 147 | switch strings.ToLower(macroLetter) { 148 | case "s": 149 | replacement = result.sender 150 | case "l": 151 | replacement = result.sender[:strings.LastIndex(result.sender, "@")] 152 | case "o": 153 | replacement = strings.TrimSuffix(result.sender[strings.LastIndex(result.sender, "@")+1:], ".") 154 | case "d": 155 | replacement = strings.TrimSuffix(domain, ".") 156 | case "i": 157 | if result.ip.To4() == nil { 158 | v6 := result.ip.To16() 159 | enc := make([]byte, 32) 160 | hex.Encode(enc, v6) 161 | var buff bytes.Buffer 162 | for i, b := range enc { 163 | if i != 0 { 164 | buff.Write([]byte{'.'}) 165 | } 166 | buff.Write([]byte{b}) 167 | } 168 | replacement = buff.String() 169 | } else { 170 | replacement = result.ip.String() 171 | } 172 | case "p": 173 | replacement = expandPtrMacro(ctx, result, domain) 174 | case "h": 175 | replacement = result.helo 176 | case "c": 177 | if !exp { 178 | return "", errors.New("c macro not allowed outside exp") 179 | } 180 | replacement = result.ip.String() 181 | case "r": 182 | if !exp { 183 | return "", errors.New("r macro not allowed outside exp") 184 | } 185 | replacement = c.Hostname 186 | case "t": 187 | if !exp { 188 | return "", errors.New("t macro not allowed outside exp") 189 | } 190 | replacement = strconv.FormatInt(time.Now().Unix(), 10) 191 | case "v": 192 | if result.ip.To4() == nil { 193 | replacement = "ip6" 194 | } else { 195 | replacement = "in-addr" 196 | } 197 | default: 198 | return "", fmt.Errorf("can't happen: impossible macro-letter: %s", macroLetter) 199 | } 200 | 201 | if macroLetter[0] >= 'A' && macroLetter[0] <= 'Z' { 202 | replacement = rfc3986Escape(replacement) 203 | } 204 | if macroLimit != "" || macroReverse != "" || macroDelimiters != "" { 205 | if macroDelimiters == "" { 206 | macroDelimiters = "." 207 | } 208 | parts := []string{} 209 | for { 210 | delimiter := strings.IndexAny(replacement, macroDelimiters) 211 | if delimiter == -1 { 212 | parts = append(parts, replacement) 213 | break 214 | } 215 | parts = append(parts, replacement[:delimiter]) 216 | replacement = replacement[delimiter+1:] 217 | } 218 | if macroReverse != "" { 219 | for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 { 220 | parts[i], parts[j] = parts[j], parts[i] 221 | } 222 | } 223 | if macroLimit != "" { 224 | limit, err := strconv.Atoi(macroLimit) 225 | if err == nil && limit < len(parts) { 226 | parts = parts[len(parts)-limit:] 227 | } 228 | } 229 | replacement = strings.Join(parts, ".") 230 | } 231 | 232 | ret.WriteString(replacement) 233 | } 234 | 235 | percent = strings.Index(domainSpec, "%") 236 | if percent == -1 { 237 | ret.WriteString(domainSpec) 238 | return ret.String(), nil 239 | } 240 | } 241 | } 242 | 243 | // ExpandDomainSpec expands a domain-spec as an SPF macro, then checks that 244 | // the result is a valid-appearing hostname. 245 | func (c *Checker) ExpandDomainSpec(ctx context.Context, domainSpec string, result *Result, domain string, exp bool) (string, error) { 246 | if domainSpec == "" { 247 | return domain, nil 248 | } 249 | target, err := c.ExpandMacro(ctx, domainSpec, result, domain, exp) 250 | if err != nil { 251 | return target, err 252 | } 253 | length := len(target) 254 | if length <= 253 { 255 | return target, nil 256 | } 257 | parts := strings.Split(target, ".") 258 | for { 259 | if len(parts) == 0 { 260 | return "", errors.New("oddly long TLD") 261 | } 262 | length = length - len(parts[0]) - 1 263 | parts = parts[1:] 264 | if length <= 253 { 265 | return strings.Join(parts, "."), nil 266 | } 267 | } 268 | } 269 | 270 | // 7.3. Macro Processing Details (rfc 7208) 271 | // Uppercase macros expand exactly as their lowercase equivalents, and 272 | // are then URL escaped. URL escaping MUST be performed for characters 273 | // not in the "unreserved" set, which is defined in [RFC3986]. 274 | 275 | // 2.3. Unreserved Characters (rfc 3986) 276 | // 277 | // Characters that are allowed in a URI but do not have a reserved 278 | // purpose are called unreserved. These include uppercase and lowercase 279 | // letters, decimal digits, hyphen, period, underscore, and tilde. 280 | // 281 | // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 282 | 283 | const upperhex = "0123456789ABCDEF" 284 | 285 | // I don't trust url.*Escape to do the right thing 286 | // code snarfed from url.escape() 287 | func rfc3986Escape(s string) string { 288 | hexCount := 0 289 | for i := 0; i < len(s); i++ { 290 | c := s[i] 291 | if shouldEscape(c) { 292 | hexCount++ 293 | } 294 | } 295 | if hexCount == 0 { 296 | return s 297 | } 298 | var buf [64]byte 299 | var t []byte 300 | 301 | required := len(s) + 2*hexCount 302 | if required <= len(buf) { 303 | t = buf[:required] 304 | } else { 305 | t = make([]byte, required) 306 | } 307 | 308 | j := 0 309 | for i := 0; i < len(s); i++ { 310 | switch c := s[i]; { 311 | case shouldEscape(c): 312 | t[j] = '%' 313 | t[j+1] = upperhex[c>>4] 314 | t[j+2] = upperhex[c&15] 315 | j += 3 316 | default: 317 | t[j] = s[i] 318 | j++ 319 | } 320 | } 321 | return string(t) 322 | } 323 | 324 | func shouldEscape(c byte) bool { 325 | switch { 326 | case 'A' <= c && c <= 'Z': 327 | return false 328 | case 'a' <= c && c <= 'z': 329 | return false 330 | case '0' <= c && c <= '9': 331 | return false 332 | } 333 | switch c { 334 | case '-', '.', '_', '~': 335 | return false 336 | } 337 | return true 338 | } 339 | -------------------------------------------------------------------------------- /testdata/openspf/nnn-spf1-test-mailzone-com-db: -------------------------------------------------------------------------------- 1 | 01.spf1-test.mailzone.com. IN TXT "v=spf1 " 2 | 02.spf1-test.mailzone.com. IN TXT "v=spf1 -all " 3 | 03.spf1-test.mailzone.com. IN TXT "v=spf1 ~all" 4 | 05.spf1-test.mailzone.com. IN TXT "v=spf1 default=deny " 5 | 06.spf1-test.mailzone.com. IN TXT "v=spf1 ?all " 6 | 07.spf1-test.mailzone.com. IN TXT "v=spf2 default=bogus " 7 | 08.spf1-test.mailzone.com. IN TXT "v=spf1 -all ?all " 8 | 09.spf1-test.mailzone.com. IN TXT "v=spf1 scope=header-from scope=envelope -all " 9 | 10.spf1-test.mailzone.com. IN MX 10 mx01.spf1-test.mailzone.com. 10 | 10.spf1-test.mailzone.com. IN MX 10 mx02.spf1-test.mailzone.com. 11 | 10.spf1-test.mailzone.com. IN MX 20 mx03.spf1-test.mailzone.com. 12 | 10.spf1-test.mailzone.com. IN TXT "v=spf1 mx -all" 13 | 11.spf1-test.mailzone.com. IN TXT "v=spf1 mx:spf1-test.mailzone.com -all" 14 | 12.spf1-test.mailzone.com. IN MX 10 mx01.spf1-test.mailzone.com. 15 | 12.spf1-test.mailzone.com. IN MX 10 mx02.spf1-test.mailzone.com. 16 | 12.spf1-test.mailzone.com. IN MX 20 mx03.spf1-test.mailzone.com. 17 | 12.spf1-test.mailzone.com. IN TXT "v=spf1 mx mx:spf1-test.mailzone.com -all" 18 | 13.spf1-test.mailzone.com. IN TXT "v=spf1 mx:spf1-test.mailzone.com mx:fallback-relay.spf1-test.mailzone.com -all" 19 | 14.spf1-test.mailzone.com. IN MX 10 mx01.spf1-test.mailzone.com. 20 | 14.spf1-test.mailzone.com. IN MX 10 mx02.spf1-test.mailzone.com. 21 | 14.spf1-test.mailzone.com. IN MX 20 mx03.spf1-test.mailzone.com. 22 | 14.spf1-test.mailzone.com. IN TXT "v=spf1 mx mx:spf1-test.mailzone.com mx:fallback-relay.spf1-test.mailzone.com -all" 23 | 20.spf1-test.mailzone.com. IN A 192.0.2.120 24 | 20.spf1-test.mailzone.com. IN TXT "v=spf1 a -all" 25 | 21.spf1-test.mailzone.com. IN A 192.0.2.121 26 | 21.spf1-test.mailzone.com. IN TXT "v=spf1 a:spf1-test.mailzone.com -all" 27 | 22.spf1-test.mailzone.com. IN A 192.0.2.122 28 | 22.spf1-test.mailzone.com. IN TXT "v=spf1 a a:spf1-test.mailzone.com -all" 29 | 30.spf1-test.mailzone.com. IN A 208.210.124.130 30 | 30.spf1-test.mailzone.com. IN TXT "v=spf1 ptr -all" 31 | 31.spf1-test.mailzone.com. IN A 208.210.124.131 32 | 31.spf1-test.mailzone.com. IN TXT "v=spf1 ptr:spf1-test.mailzone.com -all" 33 | 32.spf1-test.mailzone.com. IN A 208.210.124.132 34 | 32.spf1-test.mailzone.com. IN TXT "v=spf1 ptr ptr:spf1-test.mailzone.com -all" 35 | 40.spf1-test.mailzone.com. IN TXT "v=spf1 exists:%{ir}.%{v}._spf.%{d} -all" 36 | 41.spf1-test.mailzone.com. IN TXT "v=spf1 exists:%{ir}.%{v}._spf.spf1-test.mailzone.com -all" 37 | 42.spf1-test.mailzone.com. IN TXT "v=spf1 exists:%{ir}.%{v}._spf.%{d} exists:%{ir}.%{v}._spf.%{d3} -all" 38 | 45.spf1-test.mailzone.com. IN A 192.0.2.145 39 | 45.spf1-test.mailzone.com. IN A 192.0.2.146 40 | 45.spf1-test.mailzone.com. IN A 192.0.2.147 41 | 45.spf1-test.mailzone.com. IN TXT "v=spf1 -a a:spf1-test.mailzone.com -all" 42 | 50.spf1-test.mailzone.com. IN TXT "v=spf1 include -all" 43 | 51.spf1-test.mailzone.com. IN TXT "v=spf1 include:42.spf1-test.mailzone.com -all" 44 | 52.spf1-test.mailzone.com. IN TXT "v=spf1 include:53.spf1-test.mailzone.com -all" 45 | 53.spf1-test.mailzone.com. IN CNAME 54.spf1-test.mailzone.com. 46 | 54.spf1-test.mailzone.com. IN TXT "v=spf1 include:42.spf1-test.mailzone.com -all" 47 | 55.spf1-test.mailzone.com. IN TXT "v=spf1 include:56.spf1-test.mailzone.com -all" 48 | ;56.spf1-test.mailzone.com. RCODE=NXDOMAIN 49 | 57.spf1-test.mailzone.com. IN TXT "v=spf1 include:spf1-test.mailzone.com -all" 50 | 58.spf1-test.mailzone.com. IN TXT "v=spf1 include:59.spf1-test.mailzone.com -all" 51 | 59.spf1-test.mailzone.com. IN TXT "v=spf1 include:58.spf1-test.mailzone.com -all" 52 | 70.spf1-test.mailzone.com. IN TXT "v=spf1 exists:%{lr+=}.lp._spf.spf1-test.mailzone.com -all" 53 | 80.spf1-test.mailzone.com. IN A 208.210.124.180 54 | 80.spf1-test.mailzone.com. IN TXT "v=spf1 a mx exists:%{ir}.%{v}._spf.80.spf1-test.mailzone.com ptr -all" 55 | 90.spf1-test.mailzone.com. IN TXT "v=spf1 ip4:192.0.2.128/25 -all" 56 | 91.spf1-test.mailzone.com. IN TXT "v=spf1 -ip4:192.0.2.128/25 ip4:192.0.2.0/24 -all" 57 | 92.spf1-test.mailzone.com. IN TXT "v=spf1 ?ip4:192.0.2.192/26 ip4:192.0.2.128/25 -ip4:192.0.2.0/24 -all" 58 | 95.spf1-test.mailzone.com. IN TXT "v=spf1 exists:%{p}.whitelist.spf1-test.mailzone.com -all" 59 | 96.spf1-test.mailzone.com. IN TXT "v=spf1 -exists:%{d}.blacklist.spf1-test.mailzone.com -all" 60 | 97.spf1-test.mailzone.com. IN TXT "v=spf1 exists:%{p}.whitelist.spf1-test.mailzone.com -exists:%{d}.blacklist.spf1-test.mailzone.com -all" 61 | 98.spf1-test.mailzone.com. IN A 192.0.2.98 62 | 98.spf1-test.mailzone.com. IN MX 10 80.spf1-test.mailzone.com. 63 | 98.spf1-test.mailzone.com. IN TXT "v=spf1 a/26 mx/26 -all" 64 | 99.spf1-test.mailzone.com. IN TXT "v=spf1 -all exp=99txt.spf1-test.mailzone.com moo" 65 | 99txt.spf1-test.mailzone.com. IN TXT "u=%{u} s=%{s} d=%{d} t=%{t} h=%{h} i=%{i} %% U=%{U} S=%{S} D=%{D} T=%{T} H=%{H} I=%{I} %% moo" 66 | 100.spf1-test.mailzone.com. IN TXT "v=spf1 redirect=98.spf1-test.mailzone.com" 67 | 101.spf1-test.mailzone.com. IN TXT "v=spf1 -all redirect=98.spf1-test.mailzone.com" 68 | 102.spf1-test.mailzone.com. IN TXT "v=spf1 ?all redirect=98.spf1-test.mailzone.com" 69 | 103.spf1-test.mailzone.com. IN TXT "v=spf1 redirect=98.%{d3}" 70 | 104.spf1-test.mailzone.com. IN TXT "v=spf1 redirect=105.%{d3}" 71 | 105.spf1-test.mailzone.com. IN TXT "v=spf1 redirect=106.%{d3}" 72 | 106.spf1-test.mailzone.com. IN TXT "v=spf1 redirect=107.%{d3}" 73 | 107.spf1-test.mailzone.com. IN TXT "v=spf1 include:104.%{d3}" 74 | 110.spf1-test.mailzone.com. IN TXT "v=spf1 some:unrecognized=mechanism some=unrecognized:modifier -all" 75 | 111.spf1-test.mailzone.com. IN A 192.0.2.200 76 | 111.spf1-test.mailzone.com. IN MX 10 mx01.spf1-test.mailzone.com. 77 | 111.spf1-test.mailzone.com. IN TXT "v=spf1 mx -a gpg ~all exp=111txt.spf1-test.mailzone.com" 78 | 111txt.spf1-test.mailzone.com. IN TXT "explanation text" 79 | 112.spf1-test.mailzone.com. IN A 192.0.2.200 80 | 112.spf1-test.mailzone.com. IN TXT "v=spf1 a mp3 ~all" 81 | 113.spf1-test.mailzone.com. IN A 192.0.2.200 82 | 113.spf1-test.mailzone.com. IN TXT "v=spf1 a mp3: ~all" 83 | 114.spf1-test.mailzone.com. IN A 192.0.2.200 84 | 114.spf1-test.mailzone.com. IN MX 10 mx01.spf1-test.mailzone.com. 85 | 114.spf1-test.mailzone.com. IN TXT "v=spf1 mx -a gpg=test ~all exp=114txt.spf1-test.mailzone.com" 86 | 114txt.spf1-test.mailzone.com. IN TXT "explanation text" 87 | 115.spf1-test.mailzone.com. IN A 192.0.2.200 88 | 115.spf1-test.mailzone.com. IN TXT "v=spf1 a mp3=yes -all" 89 | 116.spf1-test.mailzone.com. IN A 192.0.2.200 90 | 116.spf1-test.mailzone.com. IN TXT "v=spf1 redirect=116rdr.spf1-test.mailzone.com a" 91 | 116rdr.spf1-test.mailzone.com. IN TXT "v=spf1 -all" 92 | 117.spf1-test.mailzone.com. IN TXT " v=spf1 +all" 93 | 118.spf1-test.mailzone.com. IN TXT "v=spf1 -all exp=" 94 | 119.spf1-test.mailzone.com. IN TXT "v=spf1 -all exp=" 95 | 119.spf1-test.mailzone.com. IN TXT "this is another txt entry that should be ignored" 96 | spf1-test.mailzone.com. IN A 192.0.2.200 97 | spf1-test.mailzone.com. IN A 208.210.124.192 98 | spf1-test.mailzone.com. IN MX 10 mx01.spf1-test.mailzone.com. 99 | spf1-test.mailzone.com. IN MX 10 mx02.spf1-test.mailzone.com. 100 | spf1-test.mailzone.com. IN MX 20 mx03.spf1-test.mailzone.com. 101 | mx01.spf1-test.mailzone.com. IN A 192.0.2.10 102 | mx01.spf1-test.mailzone.com. IN A 192.0.2.11 103 | mx01.spf1-test.mailzone.com. IN A 192.0.2.12 104 | mx01.spf1-test.mailzone.com. IN A 192.0.2.13 105 | mx02.spf1-test.mailzone.com. IN A 192.0.2.20 106 | mx02.spf1-test.mailzone.com. IN A 192.0.2.21 107 | mx02.spf1-test.mailzone.com. IN A 192.0.2.22 108 | mx02.spf1-test.mailzone.com. IN A 192.0.2.23 109 | mx03.spf1-test.mailzone.com. IN A 192.0.2.30 110 | mx03.spf1-test.mailzone.com. IN A 192.0.2.31 111 | mx03.spf1-test.mailzone.com. IN A 192.0.2.32 112 | mx03.spf1-test.mailzone.com. IN A 192.0.2.33 113 | fallback-relay.spf1-test.mailzone.com. IN MX 10 mx04.spf1-test.mailzone.com. 114 | mx04.spf1-test.mailzone.com. IN A 192.0.2.43 115 | mx04.spf1-test.mailzone.com. IN A 192.0.2.40 116 | mx04.spf1-test.mailzone.com. IN A 192.0.2.41 117 | mx04.spf1-test.mailzone.com. IN A 192.0.2.42 -------------------------------------------------------------------------------- /spf.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "os" 9 | "regexp" 10 | 11 | "github.com/miekg/dns" 12 | "strings" 13 | ) 14 | 15 | // DefaultDNSLimit is the maximum number of SPF terms that require DNS resolution to 16 | // allow before returning a failure. 17 | const DefaultDNSLimit = 10 18 | 19 | // DefaultMXAddressLimit is the maximum number of A or AAAA requests to allow while 20 | // evaluating each "mx" mechanism before returning a failure. 21 | const DefaultMXAddressLimit = 10 22 | 23 | // DefaultVoidQueryLimit is the maximum number of DNS queries that return no records 24 | // to allow before returning a failure. 25 | const DefaultVoidQueryLimit = 2 26 | 27 | // DefaultPtrAddressLimit is the limit on how many PTR records will be used when 28 | // evaluating a "ptr" mechanism or a "%{p}" macro. 29 | const DefaultPtrAddressLimit = 10 30 | 31 | // Checker holds all the configuration and limits for checking SPF records. 32 | type Checker struct { 33 | Resolver Resolver // used to resolve all DNS queries 34 | DNSLimit int // maximum number of DNS-using mechanisms 35 | MXAddressLimit int // maximum number of hostnames in an "mx" mechanism 36 | VoidQueryLimit int // maximum number of empty DNS responses 37 | PtrAddressLimit int // use only this many PTR responses 38 | Hostname string // the hostname of the machine running the check 39 | Hook Hook // instrumentation hooks 40 | } 41 | 42 | // NewChecker creates a new Checker with sensible defaults. 43 | func NewChecker() *Checker { 44 | hostname, err := os.Hostname() 45 | if err != nil { 46 | hostname = "" 47 | } 48 | return &Checker{ 49 | Resolver: &DefaultResolver{}, 50 | DNSLimit: DefaultDNSLimit, 51 | MXAddressLimit: DefaultMXAddressLimit, 52 | VoidQueryLimit: DefaultVoidQueryLimit, 53 | PtrAddressLimit: DefaultPtrAddressLimit, 54 | Hostname: hostname, 55 | } 56 | } 57 | 58 | // DefaultChecker is the Checker that will be used by the package level 59 | // spf.Check function. 60 | var DefaultChecker *Checker 61 | 62 | // Check checks SPF policy for a message using both smtp.mailfrom and smtp.helo. 63 | func Check(ctx context.Context, ip net.IP, mailFrom string, helo string) (ResultType, string) { 64 | if DefaultChecker == nil { 65 | DefaultChecker = NewChecker() 66 | } 67 | result := DefaultChecker.SPF(ctx, ip, mailFrom, helo) 68 | return result.Type, result.Explanation 69 | } 70 | 71 | // SPF checks SPF policy for a message using both smtp.mailfrom and smtp.helo. 72 | func (c *Checker) SPF(ctx context.Context, ip net.IP, mailFrom string, helo string) Result { 73 | var result Result 74 | if helo != "" { 75 | result = Result{ 76 | Type: None, 77 | ip: ip, 78 | sender: mailFrom, 79 | helo: helo, 80 | c: c, 81 | } 82 | r := c.checkHost(ctx, &result, dns.Fqdn(helo), false, false) 83 | result.Type = r 84 | if r != None && r != Neutral { 85 | result.UsedHelo = true 86 | return result 87 | } 88 | } 89 | if mailFrom != "" { 90 | result = Result{ 91 | Type: None, 92 | ip: ip, 93 | sender: mailFrom, 94 | helo: helo, 95 | c: c, 96 | } 97 | at := strings.LastIndex(mailFrom, "@") 98 | r := c.checkHost(ctx, &result, dns.Fqdn(mailFrom[at+1:]), false, false) 99 | result.Type = r 100 | } 101 | return result 102 | } 103 | 104 | // CheckHost implements the SPF check_host() function for a given domain. 105 | func (c *Checker) CheckHost(ctx context.Context, ip net.IP, domain, sender string, helo string) Result { 106 | result := Result{ 107 | Type: None, 108 | ip: ip, 109 | sender: sender, 110 | helo: helo, 111 | c: c, 112 | } 113 | 114 | result.Type = c.checkHost(ctx, &result, domain, false, false) 115 | return result 116 | } 117 | 118 | // Anything not 7 bit ascii or any control character 119 | var invalidCharRe = regexp.MustCompile(`[^ -~]`) 120 | 121 | func (c *Checker) checkHost(ctx context.Context, result *Result, domain string, include bool, redirect bool) ResultType { 122 | r := c.checkHostCore(ctx, result, domain, include, redirect) 123 | if c.Hook != nil { 124 | c.Hook.RecordResult(domain, result) 125 | } 126 | return r 127 | } 128 | 129 | // checkHost does the actual RFC 7208 check_host work 130 | func (c *Checker) checkHostCore(ctx context.Context, result *Result, domain string, include bool, redirect bool) ResultType { 131 | // 4.3 Initial Processing (RFC 7208) 132 | // If the is malformed (e.g., label longer than 63 characters, 133 | // zero-length label not at the end, etc.) or is not a multi-label 134 | // domain name, or if the DNS lookup returns "Name Error" (RCODE 3, also 135 | // known as "NXDOMAIN" [RFC2308]), check_host() immediately returns the 136 | // result "none". 137 | 138 | if _, valid := dns.IsDomainName(domain); !valid { 139 | result.Error = errors.New("invalid domain") 140 | return None 141 | } 142 | 143 | if !dns.IsFqdn(domain) { 144 | result.Error = errors.New("domain not fully qualified") 145 | return None 146 | } 147 | 148 | // 4.3 Initial Processing (RFC 7208) 149 | // If the has no local-part, substitute the string "postmaster" 150 | // for the local-part. 151 | if !strings.Contains(result.sender, "@") { 152 | result.sender = "postmaster@" + result.sender 153 | } 154 | if strings.HasPrefix(result.sender, "@") { 155 | result.sender = "postmaster" + result.sender 156 | } 157 | 158 | // 4.6.4. DNS Lookup Limits (RFC 7208) 159 | // 160 | // Some mechanisms and modifiers (collectively, "terms") cause DNS 161 | // queries at the time of evaluation, and some do not. The following 162 | // terms cause DNS queries: the "include", "a", "mx", "ptr", and 163 | // "exists" mechanisms, and the "redirect" modifier. SPF 164 | // implementations MUST limit the total number of those terms to 10 165 | // during SPF evaluation, to avoid unreasonable load on the DNS. If 166 | // this limit is exceeded, the implementation MUST return "permerror". 167 | result.DNSQueries++ 168 | if result.DNSQueries > c.DNSLimit { 169 | result.Error = fmt.Errorf("limit of %d dns queries exceeded", c.DNSLimit) 170 | return Permerror 171 | } 172 | record, resultType, err := c.getSPFRecord(ctx, domain) 173 | if err != nil { 174 | result.Error = err 175 | return resultType 176 | } 177 | if c.Hook != nil { 178 | c.Hook.Record(record, domain) 179 | } 180 | 181 | if record == "" { 182 | if redirect { 183 | return Permerror 184 | } 185 | return resultType 186 | } 187 | 188 | badChar := invalidCharRe.FindString(record) 189 | if badChar != "" { 190 | result.Error = fmt.Errorf("invalid character %q", badChar[0]) 191 | return Permerror 192 | } 193 | 194 | mechanisms, err := ParseSPF(record) 195 | if err != nil { 196 | result.Error = err 197 | return Permerror 198 | } 199 | for i, mechanism := range mechanisms.Mechanisms { 200 | resultType, err = mechanism.Evaluate(ctx, result, domain) 201 | result.Type = resultType 202 | if c.Hook != nil { 203 | c.Hook.Mechanism(domain, i, mechanism, result) 204 | } 205 | if result.DNSQueries > c.DNSLimit { 206 | result.Error = fmt.Errorf("limit of %d dns queries exceeded", c.DNSLimit) 207 | return Permerror 208 | } 209 | if resultType != None { 210 | result.Error = err 211 | if err == nil && !include && resultType == Fail && mechanisms.Exp != "" { 212 | target, err := c.ExpandDomainSpec(ctx, mechanisms.Exp, result, domain, false) 213 | if err != nil { 214 | result.Error = err 215 | return Permerror 216 | } 217 | if !validDomainName(target) { 218 | return Permerror 219 | } 220 | r := &dns.Msg{} 221 | r.SetQuestion(target, dns.TypeTXT) 222 | m, err := c.resolve(ctx, r) 223 | if err == nil && m.Rcode == dns.RcodeSuccess && len(m.Answer) == 1 { 224 | txt, ok := m.Answer[0].(*dns.TXT) 225 | if ok { 226 | result.Explanation, _ = c.ExpandMacro(ctx, strings.Join(txt.Txt, ""), result, domain, true) 227 | } 228 | } 229 | } 230 | return resultType 231 | } 232 | } 233 | 234 | // Fell off the end of the record 235 | if mechanisms.Redirect != "" { 236 | if c.Hook != nil { 237 | c.Hook.Redirect(mechanisms.Redirect) 238 | } 239 | target, err := c.ExpandDomainSpec(ctx, mechanisms.Redirect, result, domain, false) 240 | 241 | if err != nil { 242 | return Permerror 243 | } 244 | if !validDomainName(target) { 245 | return Permerror 246 | } 247 | 248 | return c.checkHost(ctx, result, dns.Fqdn(target), false, true) 249 | } 250 | return Neutral 251 | } 252 | 253 | func (c *Checker) resolve(ctx context.Context, r *dns.Msg) (*dns.Msg, error) { 254 | m, err := c.Resolver.Resolve(ctx, r) 255 | if c.Hook != nil { 256 | c.Hook.Dns(r, m, err) 257 | } 258 | return m, err 259 | } 260 | 261 | // SPFRecord holds an SPF record parsed from a single DNS TXT record. 262 | type SPFRecord struct { 263 | Mechanisms []Mechanism 264 | Exp string 265 | Redirect string 266 | OtherModifiers []string 267 | } 268 | 269 | // modifier = redirect / explanation / unknown-modifier 270 | // unknown-modifier = name "=" macro-string 271 | // ; where name is not any known modifier 272 | // 273 | // name = ALPHA *( ALPHA / DIGIT / "-" / "_" / "." ) 274 | var modifierRe = regexp.MustCompile(`^((?i)[a-z][a-z0-9_.-]*)=(.*)`) 275 | 276 | // ParseSPF parses the text of an SPF record. 277 | func ParseSPF(s string) (*SPFRecord, error) { 278 | record := &SPFRecord{} 279 | fields := strings.Fields(s) 280 | if len(fields) == 0 { 281 | return nil, errors.New("empty record") 282 | } 283 | if strings.ToLower(fields[0]) != "v=spf1" { 284 | return nil, errors.New("record doesn't begin with v=spf1") 285 | } 286 | 287 | for i, field := range fields { 288 | if i == 0 { 289 | continue 290 | } 291 | matches := modifierRe.FindStringSubmatch(field) 292 | if matches != nil { 293 | switch strings.ToLower(matches[1]) { 294 | case "redirect": 295 | if record.Redirect != "" { 296 | return nil, errors.New("multiple redirect modifiers") 297 | } 298 | if !validDomainSpec(matches[2]) { 299 | return nil, errors.New("invalid domain-spec in redirect") 300 | } 301 | record.Redirect = matches[2] 302 | case "exp": 303 | if record.Exp != "" { 304 | return nil, errors.New("multiple exp modifiers") 305 | } 306 | if !validDomainSpec(matches[2]) { 307 | return nil, errors.New("invalid domain-spec in exp") 308 | } 309 | record.Exp = matches[2] 310 | default: 311 | if !MacroIsValid(matches[2]) { 312 | return nil, errors.New("invalid macro-string in modifier") 313 | } 314 | record.OtherModifiers = append(record.OtherModifiers, field) 315 | } 316 | continue 317 | } 318 | m, err := NewMechanism(field) 319 | if err != nil { 320 | return nil, fmt.Errorf("In field '%s': %w", field, err) 321 | } 322 | record.Mechanisms = append(record.Mechanisms, m) 323 | } 324 | 325 | return record, nil 326 | } 327 | -------------------------------------------------------------------------------- /docs/rfc8616.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Internet Engineering Task Force (IETF) J. Levine 8 | Request for Comments: 8616 Taughannock Networks 9 | Updates: 6376, 7208, 7489 June 2019 10 | Category: Standards Track 11 | ISSN: 2070-1721 12 | 13 | 14 | Email Authentication for Internationalized Mail 15 | 16 | Abstract 17 | 18 | Sender Policy Framework (SPF) (RFC 7208), DomainKeys Identified Mail 19 | (DKIM) (RFC 6376), and Domain-based Message Authentication, 20 | Reporting, and Conformance (DMARC) (RFC 7489) enable a domain owner 21 | to publish email authentication and policy information in the DNS. 22 | In internationalized email, domain names can occur both as U-labels 23 | and A-labels. This specification updates the SPF, DKIM, and DMARC 24 | specifications to clarify which form of internationalized domain 25 | names to use in those specifications. 26 | 27 | Status of This Memo 28 | 29 | This is an Internet Standards Track document. 30 | 31 | This document is a product of the Internet Engineering Task Force 32 | (IETF). It represents the consensus of the IETF community. It has 33 | received public review and has been approved for publication by the 34 | Internet Engineering Steering Group (IESG). Further information on 35 | Internet Standards is available in Section 2 of RFC 7841. 36 | 37 | Information about the current status of this document, any errata, 38 | and how to provide feedback on it may be obtained at 39 | https://www.rfc-editor.org/info/rfc8616. 40 | 41 | Copyright Notice 42 | 43 | Copyright (c) 2019 IETF Trust and the persons identified as the 44 | document authors. All rights reserved. 45 | 46 | This document is subject to BCP 78 and the IETF Trust's Legal 47 | Provisions Relating to IETF Documents 48 | (https://trustee.ietf.org/license-info) in effect on the date of 49 | publication of this document. Please review these documents 50 | carefully, as they describe your rights and restrictions with respect 51 | to this document. Code Components extracted from this document must 52 | include Simplified BSD License text as described in Section 4.e of 53 | the Trust Legal Provisions and are provided without warranty as 54 | described in the Simplified BSD License. 55 | 56 | 57 | 58 | Levine Standards Track [Page 1] 59 | 60 | RFC 8616 EAI Authentication June 2019 61 | 62 | 63 | Table of Contents 64 | 65 | 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 66 | 2. Definitions . . . . . . . . . . . . . . . . . . . . . . . . . 2 67 | 3. General Principles . . . . . . . . . . . . . . . . . . . . . 3 68 | 4. SPF and Internationalized Mail . . . . . . . . . . . . . . . 3 69 | 5. DKIM and Internationalized Mail . . . . . . . . . . . . . . . 3 70 | 6. DMARC and Internationalized Mail . . . . . . . . . . . . . . 4 71 | 7. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 5 72 | 8. Security Considerations . . . . . . . . . . . . . . . . . . . 5 73 | 9. Normative References . . . . . . . . . . . . . . . . . . . . 5 74 | Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 6 75 | 76 | 1. Introduction 77 | 78 | SPF [RFC7208], DKIM [RFC6376], and DMARC [RFC7489] enable a domain 79 | owner to publish email authentication and policy information in the 80 | DNS. SPF primarily publishes information about what host addresses 81 | are authorized to send mail for a domain. DKIM places cryptographic 82 | signatures on email messages, with the validation keys published in 83 | the DNS. DMARC publishes policy information related to the domain in 84 | the From: header field of email messages. 85 | 86 | In conventional email, all domain names are ASCII in all contexts, so 87 | there is no question about the representation of the domain names. 88 | All internationalized domain names are represented as A-labels 89 | [RFC5890] in message header fields, SMTP sessions, and the DNS. 90 | 91 | Internationalized mail [RFC6530] (generally called "EAI" for Email 92 | Address Internationalization) allows U-labels in SMTP sessions 93 | [RFC6531] and message header fields [RFC6532]. 94 | 95 | Every U-label is equivalent to an A-label, so in principle, the 96 | choice of label format will not cause ambiguities. But in practice, 97 | consistent use of label formats will make it more likely that code 98 | for mail senders and receivers interoperates. 99 | 100 | Internationalized mail also allows UTF-8-encoded Unicode characters 101 | in the local parts of mailbox names, which were historically only 102 | ASCII. 103 | 104 | 2. Definitions 105 | 106 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 107 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 108 | "OPTIONAL" in this document are to be interpreted as described in 109 | BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all 110 | capitals, as shown here. 111 | 112 | 113 | 114 | Levine Standards Track [Page 2] 115 | 116 | RFC 8616 EAI Authentication June 2019 117 | 118 | 119 | The term "IDN", for Internationalized Domain Name, refers to a domain 120 | name containing either U-labels or A-labels. 121 | 122 | Since DMARC is not currently a Standards Track protocol, this 123 | specification offers advice rather than requirements for DMARC. 124 | 125 | 3. General Principles 126 | 127 | In headers in EAI mail messages, domain names that were restricted to 128 | ASCII can be U-labels, and mailbox local parts can be UTF-8. Header 129 | field names and other text intended primarily to be interpreted by 130 | computers rather than read by people remains ASCII. 131 | 132 | Strings stored in DNS records remain ASCII since there is no way to 133 | tell whether a client retrieving a DNS record expects an EAI or an 134 | ASCII result. When a domain name found in a mail header field 135 | includes U-labels, those labels are translated to A-labels before 136 | being looked up in the DNS, as described in [RFC5891]. 137 | 138 | 4. SPF and Internationalized Mail 139 | 140 | SPF [RFC7208] uses two identities from the SMTP session: the host 141 | name in the EHLO command and the domain in the address in the MAIL 142 | FROM command. Since the EHLO command precedes the server response 143 | that tells whether the server supports the SMTPUTF8 extension, an IDN 144 | host name MUST be represented as A-labels. An IDN in MAIL FROM can 145 | be either U-labels or A-labels. 146 | 147 | All U-labels MUST be converted to A-labels before being used for an 148 | SPF validation. This includes both the labels in the name used for 149 | the original DNS lookup, described in Section 3 of [RFC7208], and 150 | those used in the macro expansion of domain-spec, described in 151 | Section 7. Section 4.3 of [RFC7208] states that all IDNs in an SPF 152 | DNS record MUST be A-labels; this rule is unchanged since any SPF 153 | record can be used to authorize either EAI or conventional mail. 154 | 155 | SPF macros %{s} and %{l} expand the local part of the sender's 156 | mailbox. If the local part contains non-ASCII characters, terms that 157 | include %{s} or %{l} do not match anything, because non-ASCII local 158 | parts cannot be used as the DNS labels the macros are intended to 159 | match. Since these macros are rarely used, this is unlikely to be an 160 | issue in practice. 161 | 162 | 5. DKIM and Internationalized Mail 163 | 164 | DKIM [RFC6376] specifies a mail header field that contains a 165 | cryptographic message signature and a DNS record that contains the 166 | validation key. 167 | 168 | 169 | 170 | Levine Standards Track [Page 3] 171 | 172 | RFC 8616 EAI Authentication June 2019 173 | 174 | 175 | Section 2.11 of [RFC6376] defines dkim-quoted-printable. Its 176 | definition is modified in messages with internationalized header 177 | fields so that non-ASCII UTF-8 characters need not be quoted. The 178 | ABNF [RFC5234] for dkim-safe-char in those messages is replaced by 179 | the following, adding non-ASCII UTF-8 characters from [RFC3629]: 180 | 181 | dkim-safe-char = %x21-3A / %x3C / %x3E-7E / 182 | UTF8-2 / UTF8-3 / UTF8-4 183 | ; '!' - ':', '<', '>' - '~', non-ASCII 184 | 185 | UTF8-2 = 186 | 187 | UTF8-3 = 188 | 189 | UTF8-4 = 190 | 191 | Section 3.5 of [RFC6376] states that IDNs in the d=, i=, and s= tags 192 | of a DKIM-Signature header field MUST be encoded as A-labels. This 193 | rule is relaxed only for internationalized message header fields 194 | [RFC6532], so IDNs SHOULD be represented as U-labels. This provides 195 | improved consistency with other header fields. (A-labels remain 196 | valid to allow a transition from older software.) The set of 197 | allowable characters in the local part of an i= tag is extended in 198 | the same fashion as local parts of email addresses as described in 199 | Section 3.2 of [RFC6532]. When computing or verifying the hash in a 200 | DKIM signature as described in Section 3.7 of [RFC6376], the hash 201 | MUST use the domain name in the format it occurs in the header field. 202 | 203 | Section 3.4.2 of [RFC6376] describes relaxed header canonicalization. 204 | Its first step converts all header field names from uppercase to 205 | lowercase. Field names are restricted to printable ASCII (see 206 | [RFC5322], Section 3.6.8), so this case conversion remains ASCII case 207 | conversion. 208 | 209 | DKIM key records, described in Section 3.6.1 of [RFC6376], do not 210 | contain domain names, so there is no change to their specification. 211 | 212 | 6. DMARC and Internationalized Mail 213 | 214 | DMARC [RFC7489] defines a policy language that domain owners can 215 | specify for the domain of the address in an RFC5322.From header 216 | field. 217 | 218 | Section 6.6.1 of [RFC7489] specifies, somewhat imprecisely, how IDNs 219 | in the RFC5322.From address domain are to be handled. That section 220 | is updated to say that all U-labels in the domain are converted to 221 | A-labels before further processing. Section 7.1 of [RFC7489] is 222 | 223 | 224 | 225 | 226 | Levine Standards Track [Page 4] 227 | 228 | RFC 8616 EAI Authentication June 2019 229 | 230 | 231 | similarly updated to say that all U-labels in domains being handled 232 | are converted to A-labels before further processing. 233 | 234 | DMARC policy records, described in Sections 6.3 and 7.1 of [RFC7489], 235 | can contain email addresses in the "rua" and "ruf" tags. Since a 236 | policy record can be used for both internationalized and conventional 237 | mail, those addresses still have to be conventional addresses, not 238 | internationalized addresses. 239 | 240 | 7. IANA Considerations 241 | 242 | This document has no IANA actions. 243 | 244 | 8. Security Considerations 245 | 246 | Email is subject to a vast range of threats and abuses. This 247 | document attempts to slightly mitigate some of them but does not, as 248 | far as the author knows, add any new ones. The updates to SPF, DKIM, 249 | and DMARC are intended to allow the respective specifications to work 250 | as reliably on internationalized mail as they do on ASCII mail, so 251 | that applications that use them, such as some kinds of mail filters 252 | that catch spam and phish, can work more reliably on 253 | internationalized mail. 254 | 255 | 9. Normative References 256 | 257 | [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate 258 | Requirement Levels", BCP 14, RFC 2119, 259 | DOI 10.17487/RFC2119, March 1997, 260 | . 261 | 262 | [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO 263 | 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November 264 | 2003, . 265 | 266 | [RFC5234] Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax 267 | Specifications: ABNF", STD 68, RFC 5234, 268 | DOI 10.17487/RFC5234, January 2008, 269 | . 270 | 271 | [RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322, 272 | DOI 10.17487/RFC5322, October 2008, 273 | . 274 | 275 | [RFC5890] Klensin, J., "Internationalized Domain Names for 276 | Applications (IDNA): Definitions and Document Framework", 277 | RFC 5890, DOI 10.17487/RFC5890, August 2010, 278 | . 279 | 280 | 281 | 282 | Levine Standards Track [Page 5] 283 | 284 | RFC 8616 EAI Authentication June 2019 285 | 286 | 287 | [RFC5891] Klensin, J., "Internationalized Domain Names in 288 | Applications (IDNA): Protocol", RFC 5891, 289 | DOI 10.17487/RFC5891, August 2010, 290 | . 291 | 292 | [RFC6376] Crocker, D., Ed., Hansen, T., Ed., and M. Kucherawy, Ed., 293 | "DomainKeys Identified Mail (DKIM) Signatures", STD 76, 294 | RFC 6376, DOI 10.17487/RFC6376, September 2011, 295 | . 296 | 297 | [RFC6530] Klensin, J. and Y. Ko, "Overview and Framework for 298 | Internationalized Email", RFC 6530, DOI 10.17487/RFC6530, 299 | February 2012, . 300 | 301 | [RFC6531] Yao, J. and W. Mao, "SMTP Extension for Internationalized 302 | Email", RFC 6531, DOI 10.17487/RFC6531, February 2012, 303 | . 304 | 305 | [RFC6532] Yang, A., Steele, S., and N. Freed, "Internationalized 306 | Email Headers", RFC 6532, DOI 10.17487/RFC6532, February 307 | 2012, . 308 | 309 | [RFC7208] Kitterman, S., "Sender Policy Framework (SPF) for 310 | Authorizing Use of Domains in Email, Version 1", RFC 7208, 311 | DOI 10.17487/RFC7208, April 2014, 312 | . 313 | 314 | [RFC7489] Kucherawy, M., Ed. and E. Zwicky, Ed., "Domain-based 315 | Message Authentication, Reporting, and Conformance 316 | (DMARC)", RFC 7489, DOI 10.17487/RFC7489, March 2015, 317 | . 318 | 319 | [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 320 | 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, 321 | May 2017, . 322 | 323 | Author's Address 324 | 325 | John Levine 326 | Taughannock Networks 327 | PO Box 727 328 | Trumansburg, NY 14886 329 | United States of America 330 | 331 | Email: standards@taugh.com 332 | URI: http://jl.ly 333 | 334 | 335 | 336 | 337 | 338 | Levine Standards Track [Page 6] 339 | 340 | -------------------------------------------------------------------------------- /docs/rfc7372.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Internet Engineering Task Force (IETF) M. Kucherawy 8 | Request for Comments: 7372 September 2014 9 | Updates: 7208 10 | Category: Standards Track 11 | ISSN: 2070-1721 12 | 13 | 14 | Email Authentication Status Codes 15 | 16 | Abstract 17 | 18 | This document registers code points to allow status codes to be 19 | returned to an email client to indicate that a message is being 20 | rejected or deferred specifically because of email authentication 21 | failures. 22 | 23 | This document updates RFC 7208, since some of the code points 24 | registered replace the ones recommended for use in that document. 25 | 26 | Status of This Memo 27 | 28 | This is an Internet Standards Track document. 29 | 30 | This document is a product of the Internet Engineering Task Force 31 | (IETF). It represents the consensus of the IETF community. It has 32 | received public review and has been approved for publication by the 33 | Internet Engineering Steering Group (IESG). Further information on 34 | Internet Standards is available in Section 2 of RFC 5741. 35 | 36 | Information about the current status of this document, any errata, 37 | and how to provide feedback on it may be obtained at 38 | http://www.rfc-editor.org/info/rfc7372. 39 | 40 | Copyright Notice 41 | 42 | Copyright (c) 2014 IETF Trust and the persons identified as the 43 | document authors. All rights reserved. 44 | 45 | This document is subject to BCP 78 and the IETF Trust's Legal 46 | Provisions Relating to IETF Documents 47 | (http://trustee.ietf.org/license-info) in effect on the date of 48 | publication of this document. Please review these documents 49 | carefully, as they describe your rights and restrictions with respect 50 | to this document. Code Components extracted from this document must 51 | include Simplified BSD License text as described in Section 4.e of 52 | the Trust Legal Provisions and are provided without warranty as 53 | described in the Simplified BSD License. 54 | 55 | 56 | 57 | 58 | Kucherawy Standards Track [Page 1] 59 | 60 | RFC 7372 Email Auth Status Codes September 2014 61 | 62 | 63 | Table of Contents 64 | 65 | 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 66 | 2. Key Words . . . . . . . . . . . . . . . . . . . . . . . . . . 2 67 | 3. New Enhanced Status Codes . . . . . . . . . . . . . . . . . . 3 68 | 3.1. DKIM Failure Codes . . . . . . . . . . . . . . . . . . . 3 69 | 3.2. SPF Failure Codes . . . . . . . . . . . . . . . . . . . . 4 70 | 3.3. Reverse DNS Failure Code . . . . . . . . . . . . . . . . 5 71 | 3.4. Multiple Authentication Failures Code . . . . . . . . . . 5 72 | 4. General Considerations . . . . . . . . . . . . . . . . . . . 5 73 | 5. Security Considerations . . . . . . . . . . . . . . . . . . . 6 74 | 6. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 6 75 | 7. Normative References . . . . . . . . . . . . . . . . . . . . 7 76 | Appendix A. Acknowledgments . . . . . . . . . . . . . . . . . . 8 77 | 78 | 1. Introduction 79 | 80 | [RFC3463] introduced Enhanced Mail System Status Codes, and [RFC5248] 81 | created an IANA registry for these. 82 | 83 | [RFC6376] and [RFC7208] introduced, respectively, DomainKeys 84 | Identified Mail (DKIM) and Sender Policy Framework (SPF), two 85 | protocols for conducting message authentication. Another common 86 | email acceptance test is the reverse Domain Name System (DNS) check 87 | on an email client's IP address, as described in Section 3 of 88 | [RFC7001]. 89 | 90 | The current set of enhanced status codes does not include any code 91 | for indicating that a message is being rejected or deferred due to 92 | local policy reasons related to any of these mechanisms. This is 93 | potentially useful information to agents that need more than 94 | rudimentary handling information about the reason a message was 95 | rejected on receipt. This document introduces enhanced status codes 96 | for reporting those cases to clients. 97 | 98 | Section 3.2 updates [RFC7208], as new enhanced status codes relevant 99 | to that specification are being registered and recommended for use. 100 | 101 | 2. Key Words 102 | 103 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 104 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 105 | "OPTIONAL" in this document are to be interpreted as described in 106 | [RFC2119]. 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Kucherawy Standards Track [Page 2] 115 | 116 | RFC 7372 Email Auth Status Codes September 2014 117 | 118 | 119 | 3. New Enhanced Status Codes 120 | 121 | The new enhanced status codes are defined in the following 122 | subsections. 123 | 124 | 3.1. DKIM Failure Codes 125 | 126 | In the code point definitions below, the following definitions are 127 | used: 128 | 129 | passing: A signature is "passing" if the basic DKIM verification 130 | algorithm, as defined in [RFC6376], succeeds. 131 | 132 | acceptable: A signature is "acceptable" if it satisfies all locally 133 | defined requirements (if any) in addition to passing the basic 134 | DKIM verification algorithm (e.g., certain header fields are 135 | included in the signed content, no partial signatures, etc.). 136 | 137 | Code: X.7.20 138 | Sample Text: No passing DKIM signature found 139 | Associated basic status code: 550 140 | Description: This status code is returned when a message 141 | did not contain any passing DKIM 142 | signatures. (This violates the 143 | advice of Section 6.1 of RFC 6376.) 144 | Reference: [RFC7372]; [RFC6376] 145 | Submitter: M. Kucherawy 146 | Change controller: IESG 147 | 148 | Code: X.7.21 149 | Sample Text: No acceptable DKIM signature found 150 | Associated basic status code: 550 151 | Description: This status code is returned when a message 152 | contains one or more passing DKIM signatures, 153 | but none are acceptable. (This violates the 154 | advice of Section 6.1 of RFC 6376.) 155 | Reference: [RFC7372]; [RFC6376] 156 | Submitter: M. Kucherawy 157 | Change controller: IESG 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Kucherawy Standards Track [Page 3] 171 | 172 | RFC 7372 Email Auth Status Codes September 2014 173 | 174 | 175 | Code: X.7.22 176 | Sample Text: No valid author-matched DKIM signature found 177 | Associated basic status code: 550 178 | Description: This status code is returned when a message 179 | contains one or more passing DKIM 180 | signatures, but none are acceptable because 181 | none have an identifier(s) 182 | that matches the author address(es) found in 183 | the From header field. This is a special 184 | case of X.7.21. (This violates the advice 185 | of Section 6.1 of RFC 6376.) 186 | Reference: [RFC7372]; [RFC6376] 187 | Submitter: M. Kucherawy 188 | Change controller: IESG 189 | 190 | 3.2. SPF Failure Codes 191 | 192 | Code: X.7.23 193 | Sample Text: SPF validation failed 194 | Associated basic status code: 550 195 | Description: This status code is returned when a message 196 | completed an SPF check that produced a 197 | "fail" result, contrary to local policy 198 | requirements. Used in place of 5.7.1, as 199 | described in Section 8.4 of RFC 7208. 200 | Reference: [RFC7372]; [RFC7208] 201 | Submitter: M. Kucherawy 202 | Change controller: IESG 203 | 204 | Code: X.7.24 205 | Sample Text: SPF validation error 206 | Associated basic status code: 451/550 207 | Description: This status code is returned when evaluation 208 | of SPF relative to an arriving message 209 | resulted in an error. Used in place of 210 | 4.4.3 or 5.5.2, as described in Sections 211 | 8.6 and 8.7 of RFC 7208. 212 | Reference: [RFC7372]; [RFC7208] 213 | Submitter: M. Kucherawy 214 | Change controller: IESG 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Kucherawy Standards Track [Page 4] 227 | 228 | RFC 7372 Email Auth Status Codes September 2014 229 | 230 | 231 | 3.3. Reverse DNS Failure Code 232 | 233 | Code: X.7.25 234 | Sample Text: Reverse DNS validation failed 235 | Associated basic status code: 550 236 | Description: This status code is returned when an SMTP 237 | client's IP address failed a reverse DNS 238 | validation check, contrary to local policy 239 | requirements. 240 | Reference: [RFC7372]; Section 3 of [RFC7001] 241 | Submitter: M. Kucherawy 242 | Change controller: IESG 243 | 244 | 3.4. Multiple Authentication Failures Code 245 | 246 | Code: X.7.26 247 | Sample Text: Multiple authentication checks failed 248 | Associated basic status code: 550 249 | Description: This status code is returned when a message 250 | failed more than one message authentication 251 | check, contrary to local policy requirements. 252 | The particular mechanisms that failed are not 253 | specified. 254 | Reference: [RFC7372] 255 | Submitter: M. Kucherawy 256 | Change controller: IESG 257 | 258 | 4. General Considerations 259 | 260 | By the nature of the Simple Mail Transfer Protocol (SMTP), only one 261 | enhanced status code can be returned for a given exchange between 262 | client and server. However, an operator might decide to defer or 263 | reject a message for a plurality of reasons. Clients receiving these 264 | codes need to consider that the failure reflected by one of these 265 | status codes might not reflect the only reason, or the most important 266 | reason, for non-acceptance of the message or command. 267 | 268 | It is important to note that Section 6.1 of [RFC6376] discourages 269 | special treatment of messages bearing no valid DKIM signature. There 270 | are some operators that disregard this advice, a few of which go so 271 | far as to require a valid Author Domain Signature (that is, one 272 | matching the domain(s) in the From header field) in order to accept 273 | the message. Moreover, some nascent technologies built atop SPF and 274 | DKIM depend on such authentications. This work does not endorse 275 | configurations that violate DKIM's recommendations but rather 276 | acknowledges that they do exist and merely seeks to provide for 277 | improved interoperability with such operators. 278 | 279 | 280 | 281 | 282 | Kucherawy Standards Track [Page 5] 283 | 284 | RFC 7372 Email Auth Status Codes September 2014 285 | 286 | 287 | A specific use case for these codes is mailing list software, which 288 | processes rejections in order to remove from the subscriber set those 289 | addresses that are no longer valid. There is a need in that case to 290 | distinguish authentication failures from indications that the 291 | recipient address is no longer valid. 292 | 293 | If a receiving server performs multiple authentication checks and 294 | more than one of them fails, thus warranting rejection of the 295 | message, the SMTP server SHOULD use the code that indicates multiple 296 | methods failed rather than only reporting the first one that failed. 297 | It may be the case that one method is always expected to fail; thus, 298 | returning that method's specific code is not information useful to 299 | the sending agent. 300 | 301 | The reverse IP DNS check is defined in Section 3 of [RFC7001]. 302 | 303 | Any message authentication or policy enforcement technologies 304 | developed in the future should also include registration of their own 305 | enhanced status codes so that this kind of specific reporting is 306 | available to operators that wish to use them. 307 | 308 | 5. Security Considerations 309 | 310 | Use of these codes reveals local policy with respect to email 311 | authentication, which can be useful information to actors attempting 312 | to deliver undesired mail. It should be noted that there is no 313 | specific obligation to use these codes; if an operator wishes not to 314 | reveal this aspect of local policy, it can continue using a generic 315 | result code such as 5.7.7, 5.7.1, or even 5.7.0. 316 | 317 | 6. IANA Considerations 318 | 319 | Registration of new enhanced status codes, for addition to the 320 | Enumerated Status Codes sub-registry of the SMTP Enhanced Status 321 | Codes Registry, can be found in Section 3. 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Kucherawy Standards Track [Page 6] 339 | 340 | RFC 7372 Email Auth Status Codes September 2014 341 | 342 | 343 | 7. Normative References 344 | 345 | [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate 346 | Requirement Levels", BCP 14, RFC 2119, March 1997. 347 | 348 | [RFC3463] Vaudreuil, G., "Enhanced Mail System Status Codes", RFC 349 | 3463, January 2003. 350 | 351 | [RFC5248] Hansen, T. and J. Klensin, "A Registry for SMTP Enhanced 352 | Mail System Status Codes", BCP 138, RFC 5248, June 2008. 353 | 354 | [RFC6376] Crocker, D., Hansen, T., and M. Kucherawy, "DomainKeys 355 | Identified Mail (DKIM) Signatures", STD 76, RFC 6376, 356 | September 2011. 357 | 358 | [RFC7001] Kucherawy, M., "Message Header Field for Indicating 359 | Message Authentication Status", RFC 7001, September 2013. 360 | 361 | [RFC7208] Kitterman, S., "Sender Policy Framework (SPF) for 362 | Authorizing Use of Domains in Email, Version 1", RFC 7208, 363 | April 2014. 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | Kucherawy Standards Track [Page 7] 395 | 396 | RFC 7372 Email Auth Status Codes September 2014 397 | 398 | 399 | Appendix A. Acknowledgments 400 | 401 | Claudio Allocchio, Dave Crocker, Ned Freed, Arnt Gulbrandsen, Scott 402 | Kitterman, Barry Leiba, Alexey Melnikov, S. Moonesamy, Hector Santos, 403 | and Stephen Turnbull contributed to this work. 404 | 405 | Author's Address 406 | 407 | Murray S. Kucherawy 408 | 270 Upland Drive 409 | San Francisco, CA 94127 410 | USA 411 | 412 | EMail: superuser@gmail.com 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | Kucherawy Standards Track [Page 8] 451 | 452 | -------------------------------------------------------------------------------- /mechanism.go: -------------------------------------------------------------------------------- 1 | package spf 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/miekg/dns" 8 | "net" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // Mechanism holds an SPF mechanism 15 | type Mechanism interface { 16 | Evaluate(ctx context.Context, result *Result, domain string) (ResultType, error) 17 | String() string 18 | } 19 | 20 | var _ Mechanism = MechanismAll{} 21 | var _ Mechanism = MechanismInclude{} 22 | var _ Mechanism = MechanismA{} 23 | var _ Mechanism = MechanismMX{} 24 | var _ Mechanism = MechanismIp4{} 25 | var _ Mechanism = MechanismIp6{} 26 | var _ Mechanism = MechanismExists{} 27 | var _ Mechanism = MechanismPTR{} 28 | 29 | // 5.1. "all" 30 | // 31 | // all = "all" 32 | // 33 | // The "all" mechanism is a test that always matches. It is used as the 34 | // rightmost mechanism in a record to provide an explicit default. 35 | 36 | // MechanismAll represents an SPF "all" mechanism, it always matches. 37 | type MechanismAll struct { 38 | Qualifier ResultType 39 | } 40 | 41 | func (m MechanismAll) Evaluate(_ context.Context, _ *Result, _ string) (ResultType, error) { 42 | return m.Qualifier, nil 43 | } 44 | 45 | func (m MechanismAll) String() string { 46 | return mechanismString(m.Qualifier, "all","", net.IPMask{}, net.IPMask{}) 47 | } 48 | 49 | // 5.2. "include" 50 | // 51 | // include = "include" ":" domain-spec 52 | // 53 | // The "include" mechanism triggers a recursive evaluation of 54 | // check_host(). 55 | // 56 | // 1. The is expanded as per Section 7. 57 | // 58 | // 2. check_host() is evaluated with the resulting string as the 59 | // . The and arguments remain the same as in 60 | // the current evaluation of check_host(). 61 | // 62 | // 3. The recursive evaluation returns match, not-match, or an error. 63 | // 64 | // 4. If it returns match, then the appropriate result for the 65 | // "include" mechanism is used (e.g., include or +include produces a 66 | // "pass" result and -include produces "fail"). 67 | // 68 | // 5. If it returns not-match or an error, the parent check_host() 69 | // resumes processing as per the table below, with the previous 70 | // value of restored. 71 | 72 | // MechanismInclude represents an SPF "include" mechanism, it matches based 73 | // on the result of an SPF check on another host name. 74 | type MechanismInclude struct { 75 | Qualifier ResultType 76 | DomainSpec string 77 | } 78 | 79 | func (m MechanismInclude) Evaluate(ctx context.Context, result *Result, domain string) (ResultType, error) { 80 | dom, err := result.c.ExpandDomainSpec(ctx, m.DomainSpec, result, domain, false) 81 | 82 | if err != nil { 83 | return Permerror, err 84 | } 85 | 86 | if !validDomainName(dom) { 87 | return None, fmt.Errorf("invalid hostname '%s'", dom) 88 | } 89 | includeResult := result.c.checkHost(ctx, result, dns.Fqdn(dom), true, false) 90 | 91 | switch includeResult { 92 | case Pass: 93 | return m.Qualifier, nil 94 | case Fail, Softfail, Neutral: 95 | return None, nil 96 | case Temperror: 97 | return Temperror, nil 98 | case Permerror, None: 99 | return Permerror, nil 100 | } 101 | return Permerror, errors.New("unhandled case in MechanismInclude") 102 | } 103 | 104 | func (m MechanismInclude) String() string { 105 | return mechanismString(m.Qualifier, "include",m.DomainSpec, net.IPMask{}, net.IPMask{}) 106 | } 107 | 108 | // 5.3. "a" 109 | // 110 | // This mechanism matches if is one of the 's IP 111 | // addresses. For clarity, this means the "a" mechanism also matches 112 | // AAAA records. 113 | // 114 | // a = "a" [ ":" domain-spec ] [ dual-cidr-length ] 115 | // 116 | // An address lookup is done on the using the type of 117 | // lookup (A or AAAA) appropriate for the connection type (IPv4 or 118 | // IPv6). The is compared to the returned address(es). If any 119 | // address matches, the mechanism matches. 120 | 121 | // MechanismA represents an SPF "a" mechanism. It matches based on DNS lookups 122 | // of A and AAAA records for it's domain-spec. 123 | type MechanismA struct { 124 | Qualifier ResultType 125 | DomainSpec string 126 | Mask4 net.IPMask 127 | Mask6 net.IPMask 128 | } 129 | 130 | func (m MechanismA) Evaluate(ctx context.Context, result *Result, domain string) (ResultType, error) { 131 | result.DNSQueries++ 132 | var qtype uint16 133 | if result.ip.To4() == nil { 134 | qtype = dns.TypeAAAA 135 | } else { 136 | qtype = dns.TypeA 137 | } 138 | 139 | target, err := result.c.ExpandDomainSpec(ctx, m.DomainSpec, result, domain, false) 140 | if err != nil { 141 | return Permerror, err 142 | } 143 | if !validDomainName(target) { 144 | return None, fmt.Errorf("invalid hostname '%s'", target) 145 | } 146 | 147 | rrs, resultType, err := result.c.lookupDNS(ctx, target, qtype, result) 148 | if resultType != None { 149 | return resultType, err 150 | } 151 | 152 | for _, rr := range rrs { 153 | switch v := rr.(type) { 154 | case *dns.A: 155 | if (&net.IPNet{IP: v.A, Mask: m.Mask4}).Contains(result.ip) { 156 | return m.Qualifier, nil 157 | } 158 | case *dns.AAAA: 159 | if (&net.IPNet{IP: v.AAAA, Mask: m.Mask6}).Contains(result.ip) { 160 | return m.Qualifier, nil 161 | } 162 | } 163 | } 164 | return None, nil 165 | } 166 | 167 | func (m MechanismA) String() string { 168 | return mechanismString(m.Qualifier, "a", m.DomainSpec, m.Mask4, m.Mask6) 169 | } 170 | 171 | // 5.4. "mx" 172 | // 173 | // This mechanism matches if is one of the MX hosts for a domain 174 | // name. 175 | // 176 | // mx = "mx" [ ":" domain-spec ] [ dual-cidr-length ] 177 | // 178 | // check_host() first performs an MX lookup on the . Then 179 | // it performs an address lookup on each MX name returned. The is 180 | // compared to each returned IP address. To prevent denial-of-service 181 | // (DoS) attacks, the processing limits defined in Section 4.6.4 MUST be 182 | // followed. If the MX lookup limit is exceeded, then "permerror" is 183 | // returned and the evaluation is terminated. If any address matches, 184 | // the mechanism matches. 185 | 186 | // MechanismMX represents an SPF "mx" mechanism. It matches based on DNS lookups 187 | // of MX records for it's domain-spec, and DNS lookups for A and AAAA records 188 | // for the results of those. 189 | type MechanismMX struct { 190 | Qualifier ResultType 191 | DomainSpec string 192 | Mask4 net.IPMask 193 | Mask6 net.IPMask 194 | } 195 | 196 | 197 | func (m MechanismMX) Evaluate(ctx context.Context, result *Result, domain string) (ResultType, error) { 198 | result.DNSQueries++ 199 | var qtype uint16 200 | var mask net.IPMask 201 | if result.ip.To4() == nil { 202 | qtype = dns.TypeAAAA 203 | mask = m.Mask6 204 | } else { 205 | qtype = dns.TypeA 206 | mask = m.Mask4 207 | } 208 | 209 | target, err := result.c.ExpandDomainSpec(ctx, m.DomainSpec, result, domain, false) 210 | if err != nil { 211 | return Permerror, err 212 | } 213 | if !validDomainName(target) { 214 | return None, fmt.Errorf("invalid hostname '%s'", target) 215 | } 216 | 217 | mxrrs, resultType, err := result.c.lookupDNS(ctx, target, dns.TypeMX, result) 218 | if resultType != None { 219 | return resultType, err 220 | } 221 | 222 | mxcount := 0 223 | for _, mxrr := range mxrrs { 224 | mx := mxrr.(*dns.MX) 225 | mxcount++ 226 | if mxcount > result.c.MXAddressLimit { 227 | return Permerror, fmt.Errorf("limit of %d MX results exceeded for %s", result.c.MXAddressLimit, target) 228 | } 229 | addresses, resultType, err := result.c.lookupAddresses(ctx, mx.Mx, qtype, result) 230 | if resultType != None { 231 | return resultType, err 232 | } 233 | 234 | for _, address := range addresses { 235 | if (&net.IPNet{IP: address, Mask: mask}).Contains(result.ip) { 236 | return m.Qualifier, nil 237 | } 238 | } 239 | } 240 | 241 | return None, nil 242 | } 243 | 244 | func (m MechanismMX) String() string { 245 | return mechanismString(m.Qualifier, "mx", m.DomainSpec, m.Mask4, m.Mask6) 246 | } 247 | 248 | // 5.5. "ptr" (do not use) 249 | 250 | // MechanismPTR represents an SPF "ptr" mechanism. 251 | type MechanismPTR struct { 252 | Qualifier ResultType 253 | DomainSpec string 254 | } 255 | 256 | func (m MechanismPTR) String() string { 257 | return mechanismString(m.Qualifier, "ptr", m.DomainSpec, net.IPMask{}, net.IPMask{}) 258 | } 259 | 260 | // MechanismPtr.Evaluate is in ptr.go 261 | 262 | 263 | // 5.6. "ip4" and "ip6" 264 | // 265 | // These mechanisms test whether is contained within a given 266 | // IP network. 267 | // 268 | // ip4 = "ip4" ":" ip4-network [ ip4-cidr-length ] 269 | // ip6 = "ip6" ":" ip6-network [ ip6-cidr-length ] 270 | 271 | // MechanismIp4 represents an SPF "ip4" mechanism. It matches based on the 272 | // connecting IP being within the provided address range. 273 | type MechanismIp4 struct { 274 | Qualifier ResultType 275 | Net *net.IPNet 276 | } 277 | 278 | func (m MechanismIp4) Evaluate(_ context.Context, result *Result, _ string) (ResultType, error) { 279 | if m.Net.Contains(result.ip) { 280 | return m.Qualifier, nil 281 | } 282 | return None, nil 283 | } 284 | 285 | func (m MechanismIp4) String() string { 286 | return mechanismString(m.Qualifier, "ip4", m.Net.String(), net.IPMask{}, net.IPMask{}) 287 | } 288 | 289 | // MechanismIp6 represents an SPF "ip6" mechanism. It matches based on the 290 | // connecting IP being within the provided address range. 291 | type MechanismIp6 struct { 292 | Qualifier ResultType 293 | Net *net.IPNet 294 | } 295 | 296 | func (m MechanismIp6) Evaluate(_ context.Context, result *Result, _ string) (ResultType, error) { 297 | if m.Net.Contains(result.ip) { 298 | return m.Qualifier, nil 299 | } 300 | return None, nil 301 | } 302 | 303 | func (m MechanismIp6) String() string { 304 | return mechanismString(m.Qualifier, "ip6", m.Net.String(), net.IPMask{}, net.IPMask{}) 305 | } 306 | 307 | // 5.7. "exists" 308 | // 309 | // This mechanism is used to construct an arbitrary domain name that is 310 | // used for a DNS A record query. It allows for complicated schemes 311 | // involving arbitrary parts of the mail envelope to determine what is 312 | // permitted. 313 | // 314 | // exists = "exists" ":" domain-spec 315 | // 316 | // The is expanded as per Section 7. The resulting domain 317 | // name is used for a DNS A RR lookup (even when the connection type is 318 | // IPv6). If any A record is returned, this mechanism matches. 319 | 320 | // MechanismExists represents an SPF "exists" mechanism. It matches based on 321 | // the existence of a DNS A record for the - macro-expanded - domain-spec. 322 | type MechanismExists struct { 323 | Qualifier ResultType 324 | DomainSpec string 325 | } 326 | 327 | func (m MechanismExists) Evaluate(ctx context.Context, result *Result, domain string) (ResultType, error) { 328 | result.DNSQueries++ 329 | target, err := result.c.ExpandDomainSpec(ctx, m.DomainSpec, result, domain, false) 330 | if err != nil { 331 | return Permerror, err 332 | } 333 | if !validDomainName(target) { 334 | return None, fmt.Errorf("invalid hostname '%s'", target) 335 | } 336 | arecs, resultType, err := result.c.lookupAddresses(ctx, target, dns.TypeA, result) 337 | if resultType != None { 338 | return resultType, err 339 | } 340 | if len(arecs) == 0 { 341 | return None, nil 342 | } 343 | return m.Qualifier, nil 344 | } 345 | 346 | func (m MechanismExists) String() string { 347 | return mechanismString(m.Qualifier, "exists", m.DomainSpec, net.IPMask{}, net.IPMask{}) 348 | } 349 | 350 | 351 | // ip4-cidr-length = "/" ("0" / %x31-39 0*1DIGIT) ; value range 0-32 352 | // ip6-cidr-length = "/" ("0" / %x31-39 0*2DIGIT) ; value range 0-128 353 | // dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 354 | 355 | var v4CIDRRe = regexp.MustCompile(`/[0-9]{1,2}$`) 356 | var v6CIDRRe = regexp.MustCompile(`//[0-9]{1,3}$`) 357 | 358 | func dualCIDR(s string) (string, net.IPMask, net.IPMask, error) { 359 | loc6 := v6CIDRRe.FindStringIndex(s) 360 | 361 | var err error 362 | var v6len = 128 363 | if loc6 != nil { 364 | v6len, err = strconv.Atoi(s[loc6[0]+2:]) 365 | if err != nil || v6len > 128 { 366 | return "", nil, nil, fmt.Errorf("invalid ipv6 cidr range in dual-cidr: %s", s[loc6[0]:]) 367 | } 368 | s = s[:loc6[0]] 369 | } 370 | 371 | loc4 := v4CIDRRe.FindStringIndex(s) 372 | var v4len = 32 373 | if loc4 != nil { 374 | v4len, err = strconv.Atoi(s[loc4[0]+1:]) 375 | 376 | if err != nil || v4len > 32 { 377 | return "", nil, nil, fmt.Errorf("invalid ipv4 cidr range in dual-cidr: %s", s[loc4[0]:]) 378 | } 379 | s = s[:loc4[0]] 380 | } 381 | 382 | return s, net.CIDRMask(v4len, 32), net.CIDRMask(v6len, 128), nil 383 | } 384 | 385 | // NewMechanism creates a new Mechanism from it's text representation 386 | func NewMechanism(raw string) (Mechanism, error) { 387 | if len(raw) == 0 { 388 | return nil, errors.New("empty mechanism") 389 | } 390 | 391 | //matches := modifierRe.FindStringSubmatch(raw) 392 | //if len(matches) != 0 { 393 | // m.IsModifier = true 394 | // m.Type = strings.ToLower(matches[1]) 395 | // m.Parameter = matches[2] 396 | // return nil, nil 397 | //} 398 | 399 | // 4.6.2. Mechanisms (RFC 7208) 400 | // The possible qualifiers, and the results they cause check_host() to 401 | // return, are as follows: 402 | // 403 | // "+" pass 404 | // "-" fail 405 | // "~" softfail 406 | // "?" neutral 407 | // 408 | // The qualifier is optional and defaults to "+". 409 | 410 | var qualifier ResultType 411 | switch raw[0] { 412 | case '+': 413 | qualifier = Pass 414 | raw = raw[1:] 415 | case '-': 416 | qualifier = Fail 417 | raw = raw[1:] 418 | case '~': 419 | qualifier = Softfail 420 | raw = raw[1:] 421 | case '?': 422 | qualifier = Neutral 423 | raw = raw[1:] 424 | default: 425 | qualifier = Pass 426 | } 427 | 428 | var mtype, parameter string 429 | emptyParam := false 430 | 431 | separator := strings.IndexAny(raw, ":/") 432 | if separator == -1 { 433 | mtype = strings.ToLower(raw) 434 | } else { 435 | mtype = strings.ToLower(raw[:separator]) 436 | parameter = raw[separator:] 437 | if parameter[0] == ':' { 438 | parameter = parameter[1:] 439 | emptyParam = len(parameter) == 0 440 | } 441 | } 442 | 443 | switch mtype { 444 | case "all": 445 | if parameter != "" { 446 | return nil, errors.New("all doesn't take parameters") 447 | } 448 | return MechanismAll{Qualifier: qualifier}, nil 449 | case "include": 450 | if parameter == "" { 451 | return nil, errors.New("include requires a domain spec") 452 | } 453 | if !validDomainSpec(parameter) { 454 | return nil, errors.New("invalid domain-spec") 455 | } 456 | return MechanismInclude{ 457 | Qualifier: qualifier, 458 | DomainSpec: parameter, 459 | }, nil 460 | case "a": 461 | if emptyParam { 462 | return nil, errors.New("empty domain in a mechanism") 463 | } 464 | domainSpec, v4Mask, v6Mask, err := dualCIDR(parameter) 465 | if err != nil { 466 | return nil, err 467 | } 468 | if !validOptionalDomainSpec(domainSpec) { 469 | return nil, errors.New("invalid domain-spec") 470 | } 471 | return MechanismA{ 472 | Qualifier: qualifier, 473 | DomainSpec: domainSpec, 474 | Mask4: v4Mask, 475 | Mask6: v6Mask, 476 | }, nil 477 | case "mx": 478 | if emptyParam { 479 | return nil, errors.New("empty domain in mx mechanism") 480 | } 481 | domainSpec, v4Mask, v6Mask, err := dualCIDR(parameter) 482 | if err != nil { 483 | return nil, err 484 | } 485 | if !validOptionalDomainSpec(domainSpec) { 486 | return nil, errors.New("invalid domain-spec") 487 | } 488 | return MechanismMX{ 489 | Qualifier: qualifier, 490 | DomainSpec: domainSpec, 491 | Mask4: v4Mask, 492 | Mask6: v6Mask, 493 | }, nil 494 | case "ptr": 495 | if emptyParam { 496 | return nil, errors.New("empty domain in ptr mechanism") 497 | } 498 | if !validOptionalDomainSpec(parameter) { 499 | return nil, errors.New("invalid domain-spec") 500 | } 501 | return MechanismPTR{ 502 | Qualifier: qualifier, 503 | DomainSpec: parameter, 504 | }, nil 505 | case "ip4": 506 | addr := parameter 507 | if !strings.Contains(addr, "/") { 508 | addr = addr + "/32" 509 | } 510 | ip, cidr, err := parseCIDR(addr) 511 | if err != nil { 512 | return nil, errors.New("invalid address format") 513 | } 514 | if ip.To4() == nil { 515 | return nil, errors.New("non-IP4 address in ip4") 516 | } 517 | return MechanismIp4{ 518 | Qualifier: qualifier, 519 | Net: cidr, 520 | }, nil 521 | case "ip6": 522 | addr := parameter 523 | if !strings.Contains(addr, "/") { 524 | addr = addr + "/128" 525 | } 526 | ip, cidr, err := parseCIDR(addr) 527 | if err != nil { 528 | return nil, errors.New("invalid address format") 529 | } 530 | if len(ip) != 16 { 531 | return nil, errors.New("non-IP6 address in ip6:") 532 | } 533 | return MechanismIp6{ 534 | Qualifier: qualifier, 535 | Net: cidr, 536 | }, nil 537 | case "exists": 538 | if parameter == "" { 539 | return nil, errors.New("exists requires a domain spec") 540 | } 541 | if !validDomainSpec(parameter) { 542 | return nil, errors.New("invalid domain-spec") 543 | } 544 | return MechanismExists{ 545 | Qualifier: qualifier, 546 | DomainSpec: parameter, 547 | }, nil 548 | default: 549 | return nil, fmt.Errorf("unrecognized mechanism '%s'", mtype) 550 | } 551 | } 552 | 553 | // Stringer helpers 554 | 555 | // ResultChar maps between the spf.ResultType and the equivalent single character 556 | // qualifier used in SPF text format. 557 | var ResultChar=map[ResultType]string{ 558 | None: "", 559 | Neutral: "?", 560 | Pass: "", 561 | Fail: "-", 562 | Softfail: "~", 563 | } 564 | 565 | func mechanismString(qualifier ResultType, name string, parameter string, mask4, mask6 net.IPMask) string { 566 | var sb strings.Builder 567 | mod, ok := ResultChar[qualifier] 568 | if ok { 569 | sb.WriteString(mod) 570 | } 571 | sb.WriteString(name) 572 | if parameter != "" { 573 | sb.WriteString(":") 574 | sb.WriteString(parameter) 575 | } 576 | 577 | ones, bits := mask4.Size() 578 | if bits != 0 && ones !=32{ 579 | sb.WriteString("/") 580 | sb.WriteString(strconv.Itoa(ones)) 581 | } 582 | ones, bits = mask6.Size() 583 | if bits != 0 && ones != 128 { 584 | sb.WriteString("//") 585 | sb.WriteString(strconv.Itoa(ones)) 586 | } 587 | return sb.String() 588 | } 589 | -------------------------------------------------------------------------------- /docs/rfc8553.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Internet Engineering Task Force (IETF) D. Crocker 8 | Request for Comments: 8553 Brandenburg InternetWorking 9 | BCP: 222 March 2019 10 | Updates: 2782, 3263, 3529, 3620, 3832, 11 | 3887, 3958, 4120, 4227, 4386, 12 | 4387, 4976, 5026, 5328, 5389, 13 | 5415, 5518, 5555, 5617, 5679, 14 | 5766, 5780, 5804, 5864, 5928, 15 | 6120, 6186, 6376, 6733, 6763, 16 | 7208, 7489, 8145 17 | Category: Best Current Practice 18 | ISSN: 2070-1721 19 | 20 | 21 | DNS AttrLeaf Changes: 22 | Fixing Specifications That Use Underscored Node Names 23 | 24 | Abstract 25 | 26 | Using an underscore for a prefix creates a space for constrained 27 | interoperation of resource records. Original uses of an underscore 28 | character as a domain node name prefix were specified without the 29 | benefit of an IANA registry. This produced an entirely uncoordinated 30 | set of name-creation activities, all drawing from the same namespace. 31 | A registry for these names has now been defined by RFC 8552. 32 | However, the existing specifications that use underscored naming need 33 | to be modified in order to be in line with the new registry. This 34 | document specifies those changes. The changes preserve existing 35 | software and operational practice, while adapting the specifications 36 | for those practices to the newer underscore registry model. 37 | 38 | Status of This Memo 39 | 40 | This memo documents an Internet Best Current Practice. 41 | 42 | This document is a product of the Internet Engineering Task Force 43 | (IETF). It represents the consensus of the IETF community. It has 44 | received public review and has been approved for publication by the 45 | Internet Engineering Steering Group (IESG). Further information on 46 | BCPs is available in Section 2 of RFC 7841. 47 | 48 | Information about the current status of this document, any errata, 49 | and how to provide feedback on it may be obtained at 50 | https://www.rfc-editor.org/info/rfc8553. 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Crocker Best Current Practice [Page 1] 59 | 60 | RFC 8553 DNS AttrLeaf Fix March 2019 61 | 62 | 63 | Copyright Notice 64 | 65 | Copyright (c) 2019 IETF Trust and the persons identified as the 66 | document authors. All rights reserved. 67 | 68 | This document is subject to BCP 78 and the IETF Trust's Legal 69 | Provisions Relating to IETF Documents 70 | (https://trustee.ietf.org/license-info) in effect on the date of 71 | publication of this document. Please review these documents 72 | carefully, as they describe your rights and restrictions with respect 73 | to this document. Code Components extracted from this document must 74 | include Simplified BSD License text as described in Section 4.e of 75 | the Trust Legal Provisions and are provided without warranty as 76 | described in the Simplified BSD License. 77 | 78 | Table of Contents 79 | 80 | 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 81 | 2. Underscored RRset Use in Specifications . . . . . . . . . . . 3 82 | 2.1. TXT RRset . . . . . . . . . . . . . . . . . . . . . . . . 4 83 | 2.2. SRV RRset . . . . . . . . . . . . . . . . . . . . . . . . 5 84 | 2.3. URI RRset . . . . . . . . . . . . . . . . . . . . . . . . 6 85 | 3. Underscored Template Specifications . . . . . . . . . . . . . 7 86 | 3.1. SRV Specification Changes . . . . . . . . . . . . . . . . 7 87 | 3.2. URI Specification Changes . . . . . . . . . . . . . . . . 8 88 | 3.3. DNSSEC Signaling Specification Changes . . . . . . . . . 10 89 | 4. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 11 90 | 5. Security Considerations . . . . . . . . . . . . . . . . . . . 11 91 | 6. References . . . . . . . . . . . . . . . . . . . . . . . . . 11 92 | 6.1. Normative References . . . . . . . . . . . . . . . . . . 11 93 | 6.2. Informative References . . . . . . . . . . . . . . . . . 12 94 | Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 15 95 | Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 15 96 | 97 | 1. Introduction 98 | 99 | Original uses of an underscore character as a domain node name 100 | [RFC1035] prefix, which creates a space for constrained 101 | interpretation of resource records, were specified without the 102 | benefit of an IANA registry [IANA-reg]. This produced an entirely 103 | uncoordinated set of name-creation activities, all drawing from the 104 | same namespace. A registry has now been defined (see Section 4 of 105 | [RFC8552]); the RFC that defined it discusses the background for the 106 | use of underscored domain names [RFC8552]. 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Crocker Best Current Practice [Page 2] 115 | 116 | RFC 8553 DNS AttrLeaf Fix March 2019 117 | 118 | 119 | The basic model for underscored name registration, as specified in 120 | [RFC8552], is to have each registry entry be unique in terms of the 121 | combination of a resource record type and a "global" (highest-level) 122 | underscored node name; that is, the node name beginning with an 123 | underscore that is the closest to the DNS root. 124 | 125 | The specifications describing the existing uses of underscored naming 126 | do not reflect the existence of this integrated registry. For the 127 | new reader or the new editor of one of those documents, there is 128 | currently nothing signaling that the underscored name(s) defined in 129 | the document are now processed through an IANA registry. This 130 | document remedies that, by marking such a published document with an 131 | update that indicates the nature of the change. 132 | 133 | Further, the documents that define the SRV [RFC2782] and URI 134 | [RFC7553] DNS resource records provide a meta-template for 135 | underscored name assignments, partially based on separate registries 136 | [RFC6335]. For the portion that selects the global (highest-level) 137 | underscored node name, this perpetuates uncoordinated assignment 138 | activities by separate technical specifications, out of the same 139 | namespace. This document remedies that by providing detail for 140 | revisions to the SRV and URI specifications to bring their use in 141 | line with the single, integrated "Underscored and Globally Scoped DNS 142 | Node Names" registry. 143 | 144 | The result of these changes preserves existing software and 145 | operations practices while adapting the technical specifications to 146 | the newer underscore registry model. 147 | 148 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 149 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 150 | "OPTIONAL" in this document are to be interpreted as described in 151 | BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all 152 | capitals, as shown here. 153 | 154 | 2. Underscored RRset Use in Specifications 155 | 156 | The use of underscored node names is specific to each RR TYPE that is 157 | being scoped. Each name defines a place but does not define the 158 | rules for what appears underneath that place, either as additional 159 | underscored naming or as a leaf node with resource records. Details 160 | for those rules are provided by specifications for individual RR 161 | TYPEs. The sections below describe the way that existing underscored 162 | names are used with the RR TYPEs that they name. 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Crocker Best Current Practice [Page 3] 171 | 172 | RFC 8553 DNS AttrLeaf Fix March 2019 173 | 174 | 175 | 2.1. TXT RRset 176 | 177 | 178 | 179 | NOTE - Documents falling into this category include: [RFC5518], 180 | [RFC5617], [RFC6120], [RFC6376], [RFC6763], [RFC7208], and 181 | [RFC7489]. 182 | 183 | This section provides a generic approach for changes to existing 184 | specifications that define straightforward use of underscored node 185 | names when scoping the use of a TXT RRset. The approach provides the 186 | information needed for adapting such specifications to the use of the 187 | IANA "Underscored and Globally Scoped DNS Node Names" registry 188 | [RFC8552]. Hence, the approach is meant both as an update to these 189 | existing specifications and as guidance for changes when those 190 | documents are revised. 191 | 192 | For any document that specifies the use of a TXT RRset under one or 193 | more underscored names, the global node name is expected to be 194 | registered in the IANA "Underscored and Globally Scoped DNS Node 195 | Names" registry [RFC8552]. An effort has been made to locate 196 | existing documents that do this, to register the global underscored 197 | node names, and to list them in the initial set of names added to the 198 | registry. 199 | 200 | If a public specification defines use of a TXT RRset and calls for 201 | the use of an underscored node name, here is a template of suggested 202 | text for registering the global underscored node name -- the one 203 | closest to the root -- that can be used through the IANA 204 | Considerations section of the specification: 205 | 206 | "Per [RFC8552], please add the following entry to the "Underscored 207 | and Globally Scoped DNS Node Names" registry:" 208 | 209 | +--------+----------------+-----------------------------------------+ 210 | | RR | _NODE NAME | Reference | 211 | | Type | | | 212 | +--------+----------------+-----------------------------------------+ 213 | | TXT | _{DNS node | {citation for the document making the | 214 | | | name} | addition} | 215 | +--------+----------------+-----------------------------------------+ 216 | 217 | Table 1: Entry for the "Underscored and Globally Scoped DNS 218 | Node Names" Registry for TXT RR Use 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Crocker Best Current Practice [Page 4] 227 | 228 | RFC 8553 DNS AttrLeaf Fix March 2019 229 | 230 | 231 | 2.2. SRV RRset 232 | 233 | NOTE - Documents falling into this category include: 234 | 235 | [RFC3263], [RFC3529], [RFC3620], [RFC3832], [RFC3887], 236 | [RFC3958], [RFC4120], [RFC4227], [RFC4386], [RFC4387], 237 | [RFC4976], [RFC5026], [RFC5328], [RFC5389], [RFC5415], 238 | [RFC5555], [RFC5679], [RFC5766], [RFC5780], [RFC5804], 239 | [RFC5864], [RFC5928], and [RFC6186]. 240 | 241 | Specification of the SRV resource record [RFC2782] provides a 242 | template for use of underscored node names. The global node name is 243 | characterized as referencing the 'protocol' that is associated with 244 | SRV RRset usage. 245 | 246 | This section provides a generic approach for changes to existing 247 | specifications that define the use of an SRV RRset. The approach 248 | provides the information needed for adapting such specifications to 249 | the use of the IANA "Underscored and Globally Scoped DNS Node Names" 250 | registry [RFC8552]. Hence, the approach is meant both as an update 251 | to these existing specifications and as guidance for changes when 252 | those documents are revised. 253 | 254 | For any document that specifies the use of an SRV RRset, the global 255 | ('protocol') underscored node name is expected to be registered in 256 | the IANA "Underscored and Globally Scoped DNS Node Names" registry 257 | [RFC8552]. An effort has been made to locate existing documents that 258 | do this, to register the global underscored node names, and to list 259 | them in the initial set of names added to the registry. 260 | 261 | If a public specification defines use of an SRV RRset and calls for 262 | the use of an underscored node name, here is a template of suggested 263 | text for registering the global underscored node name -- the one 264 | closest to the root -- that can be used through the IANA 265 | Considerations section of the specification: 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | Crocker Best Current Practice [Page 5] 283 | 284 | RFC 8553 DNS AttrLeaf Fix March 2019 285 | 286 | 287 | "Per [RFC8552], please add the following entry to the "Underscored 288 | and Globally Scoped DNS Node Names" registry: 289 | 290 | +--------+----------------------+-----------------------------------+ 291 | | RR | _NODE NAME | Reference | 292 | | Type | | | 293 | +--------+----------------------+-----------------------------------+ 294 | | SRV | _{DNS 'protocol' | {citation for the document making | 295 | | | node name} | the addition} | 296 | +--------+----------------------+-----------------------------------+ 297 | 298 | Table 2: Entry for the "Underscored and Globally Scoped DNS Node 299 | Names" Registry for SRV RR Use 300 | 301 | 2.3. URI RRset 302 | 303 | Specification of the URI resource record [RFC7553] provides a 304 | template for use of underscored node names. The global node name is 305 | characterized as naming the 'protocol' that is associated with URI RR 306 | usage or by reversing an Enumservice sequence [RFC6117]. 307 | 308 | This section provides a generic approach for changes to existing 309 | specifications that define use of a URI RRset. The approach provides 310 | the information needed for adapting such specifications to the use of 311 | the IANA "Underscored and Globally Scoped DNS Node Names" registry 312 | [RFC8552]. Hence, the approach is meant both as an update to these 313 | existing specifications and as guidance for changes when those 314 | documents are revised. 315 | 316 | For any document that specifies the use of a URI RRset, the global 317 | ('protocol' or highest-level Enumservice) underscored node name is 318 | expected to be registered in the IANA "Underscored and Globally 319 | Scoped DNS Node Names" registry [RFC8552]. An effort has been made 320 | to locate existing documents that do this, to register the global 321 | underscored node names, and to list them in the initial set of names 322 | added to the registry. 323 | 324 | If a public specification defines use of a URI RRset and calls for 325 | the use of an underscored node name, here is a template of suggested 326 | text for registering the global underscored node name -- the one 327 | closest to the root -- that can be used through the IANA 328 | Considerations section of the specification: 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Crocker Best Current Practice [Page 6] 339 | 340 | RFC 8553 DNS AttrLeaf Fix March 2019 341 | 342 | 343 | "Per [RFC8552], please add the following entry to the "Underscored 344 | and Globally Scoped DNS Node Names" registry: 345 | 346 | +-------+----------------------------+------------------------------+ 347 | | RR | _NODE NAME | Reference | 348 | | Type | | | 349 | +-------+----------------------------+------------------------------+ 350 | | URI | _{DNS 'protocol' or | {citation for the document | 351 | | | Enumservice node name} | making the addition} | 352 | +-------+----------------------------+------------------------------+ 353 | 354 | Table 3: Entry for the "Underscored and Globally Scoped DNS Node 355 | Names" Registry for URI RR Use 356 | 357 | 3. Underscored Template Specifications 358 | 359 | 3.1. SRV Specification Changes 360 | 361 | The specification for a domain name, under which an SRV resource 362 | record [RFC2782] appears, provides a template for use of underscored 363 | node names. The global underscored node name is characterized as 364 | indicating the 'protocol' that is associated with SRV RR usage. 365 | 366 | The text of [RFC2782] is changed as described below. In addition, 367 | note that a normative reference to RFC 8552 is added to the 368 | References section of RFC 2782. 369 | 370 | OLD: 371 | 372 | The format of the SRV RR 373 | 374 | Here is the format of the SRV RR, whose DNS type code is 33: 375 | _Service._Proto.Name TTL Class SRV Priority Weight Port Target 376 | ... 377 | Proto 378 | The symbolic name of the desired protocol, with an underscore 379 | (_) prepended to prevent collisions with DNS labels that occur 380 | in nature. _TCP and _UDP are at present the most useful values 381 | for this field, though any name defined by Assigned Numbers or 382 | locally may be used (as for Service). The Proto is case 383 | insensitive. 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | Crocker Best Current Practice [Page 7] 395 | 396 | RFC 8553 DNS AttrLeaf Fix March 2019 397 | 398 | 399 | NEW: 400 | 401 | The format of the SRV RR 402 | 403 | Here is the format of the SRV RR, whose DNS type code is 33: 404 | 405 | "_Service._Proto.Name TTL Class SRV Priority Weight Port 406 | Target" 407 | 408 | _..._ 409 | 410 | Proto 411 | 412 | The symbolic name of the desired protocol with an underscore 413 | (e.g., "_name") prepended to prevent collisions with DNS 414 | labels that occur in nature. _TCP and _UDP are at present 415 | the most useful values for this field. The Proto is case 416 | insensitive. 417 | 418 | The SRV RRset 'protocol' (global) underscored node name 419 | SHOULD be registered in the IANA "Underscored and Globally 420 | Scoped DNS Node Names" registry [RFC8552]. 421 | 422 | 3.2. URI Specification Changes 423 | 424 | Specification for the domain name (under which a URI resource record 425 | [RFC7553] occurs) is similar to that for the SRV resource record 426 | [RFC2782], although the text refers only to 'service' name, rather 427 | than distinguishing 'service' from 'protocol'. Further, the URI RR 428 | specification permits alternative underscored naming schemes: 429 | 430 | One matches what is used for SRV, with the global underscored node 431 | name called 'protocol'. 432 | 433 | The other is based on a reversing of an Enumservice [RFC6117] 434 | sequence. 435 | 436 | Text of [RFC7553] is changed as described below. In addition, a 437 | normative reference to RFC 8552 is added to the References section of 438 | RFC 7553. 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | Crocker Best Current Practice [Page 8] 451 | 452 | RFC 8553 DNS AttrLeaf Fix March 2019 453 | 454 | 455 | OLD: 456 | 457 | 4.1. Owner Name, Class, and Type 458 | 459 | The URI owner name is subject to special conventions. 460 | 461 | Just like the SRV RR [RFC2782], the URI RR has service information 462 | encoded in its owner name. In order to encode the service for a 463 | specific owner name, one uses service parameters. Valid service 464 | parameters are those registered by IANA in the "Service Name and 465 | Transport Protocol Port Number Registry" [RFC6335] or as "Enumservice 466 | --- 467 | Registrations [RFC6117]. The Enumservice Registration parameters are 468 | reversed (i.e., subtype(s) before type), prepended with an underscore 469 | (_), and prepended to the owner name in separate labels. The 470 | underscore is prepended to the service parameters to avoid collisions 471 | with DNS labels that occur in nature, and the order is reversed to 472 | make it possible to do delegations, if needed, to different zones 473 | (and therefore providers of DNS). 474 | 475 | For example, suppose we are looking for the URI for a service with 476 | ENUM Service Parameter "A:B:C" for host example.com. Then we would 477 | query for (QNAME,QTYPE)=("_C._B._A.example.com","URI"). 478 | 479 | As another example, suppose we are looking for the URI for a service 480 | with Service Name "A" and Transport Protocol "B" for host 481 | example.com. Then we would query for 482 | (QNAME,QTYPE)=("_A._B.example.com","URI"). 483 | 484 | NEW: 485 | 486 | 4.1. Owner Name, Class, and Type 487 | 488 | The URI owner name is subject to special conventions. 489 | 490 | As for the SRV RRset [RFC2782], the URI RRset global (highest- 491 | level) underscored node name SHOULD be registered in the IANA 492 | "Underscored and Globally Scoped DNS Node Names" registry 493 | [RFC8552]. 494 | 495 | Just like the SRV RRset, the URI RRset has service information 496 | encoded in its owner name. In order to encode the service for 497 | a specific owner name, one uses service parameters. Valid 498 | service parameters are: 499 | 500 | + Those registered by IANA in the "Service Name and Transport 501 | Protocol Port Number Registry" [RFC6335]. The underscore is 502 | prepended to the service parameters to avoid collisions with 503 | 504 | 505 | 506 | Crocker Best Current Practice [Page 9] 507 | 508 | RFC 8553 DNS AttrLeaf Fix March 2019 509 | 510 | 511 | DNS labels that occur in nature, and the order is reversed 512 | to make it possible to do delegations, if needed, to 513 | different zones (and therefore providers of DNS). 514 | 515 | + Those listed in "Enumservice Registrations" [RFC6117]. The 516 | Enumservice Registration parameters are reversed (i.e., 517 | subtype(s) before type), prepended with an underscore (e.g., 518 | "_name"), and prepended to the owner name in separate 519 | labels. The highest-level (global) underscored Enumservice 520 | name becomes the global name per RFC 8552 to register. 521 | 522 | For example, suppose we are looking for the URI for a service 523 | with ENUM Service Parameter "A:B:C" for host example.com. Then 524 | we would query for 525 | (QNAME,QTYPE)=("_C._B._A.example.com","URI"). 526 | 527 | As another example, suppose we are looking for the URI for a 528 | service with Service Name "A" and Transport Protocol "B" for 529 | host example.com. Then we would query for 530 | (QNAME,QTYPE)=("_A._B.example.com","URI"). 531 | 532 | 3.3. DNSSEC Signaling Specification Changes 533 | 534 | "Signaling Trust Anchor Knowledge in DNS Security Extensions 535 | (DNSSEC)" [RFC8145] defines a use of DNS node names that effectively 536 | consumes all names beginning with the string "_ta-" when using the 537 | NULL RR in the query. 538 | 539 | Text of Section 5.1, "Query Format", of RFC 8145 is changed as 540 | described below. In addition, a normative reference to RFC 8552 is 541 | added to the References section of RFC 8145. 542 | 543 | OLD: 544 | 545 | For example, a validating DNS resolver ... 546 | QNAME=_ta-4444. 547 | 548 | NEW: 549 | 550 | For example, a validating DNS resolver ... "QNAME=_ta-4444". 551 | 552 | Under the NULL RR, an entry is registered in the IANA 553 | "Underscored and Globally Scoped DNS Node Names" registry 554 | [RFC8552] for all node names beginning with "_ta-". 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | Crocker Best Current Practice [Page 10] 563 | 564 | RFC 8553 DNS AttrLeaf Fix March 2019 565 | 566 | 567 | 4. IANA Considerations 568 | 569 | Although this document makes reference to IANA registries, it 570 | introduces no new IANA registries or procedures. 571 | 572 | 5. Security Considerations 573 | 574 | This memo raises no security issues. 575 | 576 | 6. References 577 | 578 | 6.1. Normative References 579 | 580 | [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate 581 | Requirement Levels", BCP 14, RFC 2119, 582 | DOI 10.17487/RFC2119, March 1997, 583 | . 584 | 585 | [RFC6117] Hoeneisen, B., Mayrhofer, A., and J. Livingood, "IANA 586 | Registration of Enumservices: Guide, Template, and IANA 587 | Considerations", RFC 6117, DOI 10.17487/RFC6117, March 588 | 2011, . 589 | 590 | [RFC6335] Cotton, M., Eggert, L., Touch, J., Westerlund, M., and S. 591 | Cheshire, "Internet Assigned Numbers Authority (IANA) 592 | Procedures for the Management of the Service Name and 593 | Transport Protocol Port Number Registry", BCP 165, 594 | RFC 6335, DOI 10.17487/RFC6335, August 2011, 595 | . 596 | 597 | [RFC7553] Faltstrom, P. and O. Kolkman, "The Uniform Resource 598 | Identifier (URI) DNS Resource Record", RFC 7553, 599 | DOI 10.17487/RFC7553, June 2015, 600 | . 601 | 602 | [RFC8145] Wessels, D., Kumari, W., and P. Hoffman, "Signaling Trust 603 | Anchor Knowledge in DNS Security Extensions (DNSSEC)", 604 | RFC 8145, DOI 10.17487/RFC8145, April 2017, 605 | . 606 | 607 | [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 608 | 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, 609 | May 2017, . 610 | 611 | [RFC8552] Crocker, D., "Scoped Interpretation of DNS Resource 612 | Records through "Underscored" Naming of Attribute Leaves", 613 | RFC 8552, DOI 10.17487/RFC8552, March 2019, 614 | . 615 | 616 | 617 | 618 | Crocker Best Current Practice [Page 11] 619 | 620 | RFC 8553 DNS AttrLeaf Fix March 2019 621 | 622 | 623 | 6.2. Informative References 624 | 625 | [IANA-reg] 626 | IANA, "Protocol Registries", 627 | . 628 | 629 | [RFC1035] Mockapetris, P., "Domain names - implementation and 630 | specification", STD 13, RFC 1035, DOI 10.17487/RFC1035, 631 | November 1987, . 632 | 633 | [RFC2782] Gulbrandsen, A., Vixie, P., and L. Esibov, "A DNS RR for 634 | specifying the location of services (DNS SRV)", RFC 2782, 635 | DOI 10.17487/RFC2782, February 2000, 636 | . 637 | 638 | [RFC3263] Rosenberg, J. and H. Schulzrinne, "Session Initiation 639 | Protocol (SIP): Locating SIP Servers", RFC 3263, 640 | DOI 10.17487/RFC3263, June 2002, 641 | . 642 | 643 | [RFC3529] Harold, W., "Using Extensible Markup Language-Remote 644 | Procedure Calling (XML-RPC) in Blocks Extensible Exchange 645 | Protocol (BEEP)", RFC 3529, DOI 10.17487/RFC3529, April 646 | 2003, . 647 | 648 | [RFC3620] New, D., "The TUNNEL Profile", RFC 3620, 649 | DOI 10.17487/RFC3620, October 2003, 650 | . 651 | 652 | [RFC3832] Zhao, W., Schulzrinne, H., Guttman, E., Bisdikian, C., and 653 | W. Jerome, "Remote Service Discovery in the Service 654 | Location Protocol (SLP) via DNS SRV", RFC 3832, 655 | DOI 10.17487/RFC3832, July 2004, 656 | . 657 | 658 | [RFC3887] Hansen, T., "Message Tracking Query Protocol", RFC 3887, 659 | DOI 10.17487/RFC3887, September 2004, 660 | . 661 | 662 | [RFC3958] Daigle, L. and A. Newton, "Domain-Based Application 663 | Service Location Using SRV RRs and the Dynamic Delegation 664 | Discovery Service (DDDS)", RFC 3958, DOI 10.17487/RFC3958, 665 | January 2005, . 666 | 667 | [RFC4120] Neuman, C., Yu, T., Hartman, S., and K. Raeburn, "The 668 | Kerberos Network Authentication Service (V5)", RFC 4120, 669 | DOI 10.17487/RFC4120, July 2005, 670 | . 671 | 672 | 673 | 674 | Crocker Best Current Practice [Page 12] 675 | 676 | RFC 8553 DNS AttrLeaf Fix March 2019 677 | 678 | 679 | [RFC4227] O'Tuathail, E. and M. Rose, "Using the Simple Object 680 | Access Protocol (SOAP) in Blocks Extensible Exchange 681 | Protocol (BEEP)", RFC 4227, DOI 10.17487/RFC4227, January 682 | 2006, . 683 | 684 | [RFC4386] Boeyen, S. and P. Hallam-Baker, "Internet X.509 Public Key 685 | Infrastructure Repository Locator Service", RFC 4386, 686 | DOI 10.17487/RFC4386, February 2006, 687 | . 688 | 689 | [RFC4387] Gutmann, P., Ed., "Internet X.509 Public Key 690 | Infrastructure Operational Protocols: Certificate Store 691 | Access via HTTP", RFC 4387, DOI 10.17487/RFC4387, February 692 | 2006, . 693 | 694 | [RFC4976] Jennings, C., Mahy, R., and A. Roach, "Relay Extensions 695 | for the Message Sessions Relay Protocol (MSRP)", RFC 4976, 696 | DOI 10.17487/RFC4976, September 2007, 697 | . 698 | 699 | [RFC5026] Giaretta, G., Ed., Kempf, J., and V. Devarapalli, Ed., 700 | "Mobile IPv6 Bootstrapping in Split Scenario", RFC 5026, 701 | DOI 10.17487/RFC5026, October 2007, 702 | . 703 | 704 | [RFC5328] Adolf, A. and P. MacAvock, "A Uniform Resource Name (URN) 705 | Namespace for the Digital Video Broadcasting Project 706 | (DVB)", RFC 5328, DOI 10.17487/RFC5328, September 2008, 707 | . 708 | 709 | [RFC5389] Rosenberg, J., Mahy, R., Matthews, P., and D. Wing, 710 | "Session Traversal Utilities for NAT (STUN)", RFC 5389, 711 | DOI 10.17487/RFC5389, October 2008, 712 | . 713 | 714 | [RFC5415] Calhoun, P., Ed., Montemurro, M., Ed., and D. Stanley, 715 | Ed., "Control And Provisioning of Wireless Access Points 716 | (CAPWAP) Protocol Specification", RFC 5415, 717 | DOI 10.17487/RFC5415, March 2009, 718 | . 719 | 720 | [RFC5518] Hoffman, P., Levine, J., and A. Hathcock, "Vouch By 721 | Reference", RFC 5518, DOI 10.17487/RFC5518, April 2009, 722 | . 723 | 724 | [RFC5555] Soliman, H., Ed., "Mobile IPv6 Support for Dual Stack 725 | Hosts and Routers", RFC 5555, DOI 10.17487/RFC5555, June 726 | 2009, . 727 | 728 | 729 | 730 | Crocker Best Current Practice [Page 13] 731 | 732 | RFC 8553 DNS AttrLeaf Fix March 2019 733 | 734 | 735 | [RFC5617] Allman, E., Fenton, J., Delany, M., and J. Levine, 736 | "DomainKeys Identified Mail (DKIM) Author Domain Signing 737 | Practices (ADSP)", RFC 5617, DOI 10.17487/RFC5617, August 738 | 2009, . 739 | 740 | [RFC5679] Bajko, G., "Locating IEEE 802.21 Mobility Services Using 741 | DNS", RFC 5679, DOI 10.17487/RFC5679, December 2009, 742 | . 743 | 744 | [RFC5766] Mahy, R., Matthews, P., and J. Rosenberg, "Traversal Using 745 | Relays around NAT (TURN): Relay Extensions to Session 746 | Traversal Utilities for NAT (STUN)", RFC 5766, 747 | DOI 10.17487/RFC5766, April 2010, 748 | . 749 | 750 | [RFC5780] MacDonald, D. and B. Lowekamp, "NAT Behavior Discovery 751 | Using Session Traversal Utilities for NAT (STUN)", 752 | RFC 5780, DOI 10.17487/RFC5780, May 2010, 753 | . 754 | 755 | [RFC5804] Melnikov, A., Ed. and T. Martin, "A Protocol for Remotely 756 | Managing Sieve Scripts", RFC 5804, DOI 10.17487/RFC5804, 757 | July 2010, . 758 | 759 | [RFC5864] Allbery, R., "DNS SRV Resource Records for AFS", RFC 5864, 760 | DOI 10.17487/RFC5864, April 2010, 761 | . 762 | 763 | [RFC5928] Petit-Huguenin, M., "Traversal Using Relays around NAT 764 | (TURN) Resolution Mechanism", RFC 5928, 765 | DOI 10.17487/RFC5928, August 2010, 766 | . 767 | 768 | [RFC6120] Saint-Andre, P., "Extensible Messaging and Presence 769 | Protocol (XMPP): Core", RFC 6120, DOI 10.17487/RFC6120, 770 | March 2011, . 771 | 772 | [RFC6186] Daboo, C., "Use of SRV Records for Locating Email 773 | Submission/Access Services", RFC 6186, 774 | DOI 10.17487/RFC6186, March 2011, 775 | . 776 | 777 | [RFC6376] Crocker, D., Ed., Hansen, T., Ed., and M. Kucherawy, Ed., 778 | "DomainKeys Identified Mail (DKIM) Signatures", STD 76, 779 | RFC 6376, DOI 10.17487/RFC6376, September 2011, 780 | . 781 | 782 | 783 | 784 | 785 | 786 | Crocker Best Current Practice [Page 14] 787 | 788 | RFC 8553 DNS AttrLeaf Fix March 2019 789 | 790 | 791 | [RFC6763] Cheshire, S. and M. Krochmal, "DNS-Based Service 792 | Discovery", RFC 6763, DOI 10.17487/RFC6763, February 2013, 793 | . 794 | 795 | [RFC7208] Kitterman, S., "Sender Policy Framework (SPF) for 796 | Authorizing Use of Domains in Email, Version 1", RFC 7208, 797 | DOI 10.17487/RFC7208, April 2014, 798 | . 799 | 800 | [RFC7489] Kucherawy, M., Ed. and E. Zwicky, Ed., "Domain-based 801 | Message Authentication, Reporting, and Conformance 802 | (DMARC)", RFC 7489, DOI 10.17487/RFC7489, March 2015, 803 | . 804 | 805 | Acknowledgements 806 | 807 | Thanks go to Bill Fenner, Dick Franks, Tony Hansen, Peter Koch, Olaf 808 | Kolkman, and Andrew Sullivan for diligent review of the (much) 809 | earlier draft versions. For the later enhancements, thanks to Tim 810 | Wicinski, John Levine, Bob Harold, Joel Jaeggli, Ondrej Sury, and 811 | Paul Wouters. 812 | 813 | Special thanks to Ray Bellis for his persistent encouragement to 814 | continue this effort, as well as the suggestion for an essential 815 | simplification to the registration model. 816 | 817 | Author's Address 818 | 819 | Dave Crocker 820 | Brandenburg InternetWorking 821 | 675 Spruce Dr. 822 | Sunnyvale, CA 94086 823 | United States of America 824 | 825 | Phone: +1.408.246.8253 826 | Email: dcrocker@bbiw.net 827 | URI: http://bbiw.net/ 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | Crocker Best Current Practice [Page 15] 843 | 844 | --------------------------------------------------------------------------------