mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
in dkim-signature header, allow FWS anywhere in "z=" (copied headers), and prevent panic in cli command "mox dkim verify" when a dkim-signature cannot be parsed
the BNF for "z=" does not show FWS is allowed (while it does in other places, eg base64), but the text above the BNF explains it in words.
This commit is contained in:
parent
2239f38232
commit
ba077dadd0
3 changed files with 67 additions and 40 deletions
|
@ -51,13 +51,21 @@ func (p *parser) xtaken(n int) string {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) xtakefn(fn func(c rune, i int) bool) string {
|
func (p *parser) xtakefn(ignoreFWS bool, fn func(c rune, i int) bool) string {
|
||||||
|
var r string
|
||||||
for i, c := range p.s[p.o:] {
|
for i, c := range p.s[p.o:] {
|
||||||
if !fn(c, i) {
|
if !fn(c, i) {
|
||||||
return p.xtaken(i)
|
switch c {
|
||||||
|
case ' ', '\t', '\r', '\n':
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.xtaken(i)
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
r += string(c)
|
||||||
}
|
}
|
||||||
return p.xtaken(len(p.s) - p.o)
|
p.xtaken(len(p.s) - p.o)
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) empty() bool {
|
func (p *parser) empty() bool {
|
||||||
|
@ -70,21 +78,28 @@ func (p *parser) xnonempty() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) xtakefn1(fn func(c rune, i int) bool) string {
|
func (p *parser) xtakefn1(ignoreFWS bool, fn func(c rune, i int) bool) string {
|
||||||
|
var r string
|
||||||
p.xnonempty()
|
p.xnonempty()
|
||||||
for i, c := range p.s[p.o:] {
|
for i, c := range p.s[p.o:] {
|
||||||
if !fn(c, i) {
|
if !fn(c, i) {
|
||||||
|
switch c {
|
||||||
|
case ' ', '\t', '\r', '\n':
|
||||||
|
continue
|
||||||
|
}
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
p.xerrorf("expected at least 1 char")
|
p.xerrorf("expected at least 1 char")
|
||||||
}
|
}
|
||||||
return p.xtaken(i)
|
p.xtaken(i)
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
r += string(c)
|
||||||
}
|
}
|
||||||
return p.xtaken(len(p.s) - p.o)
|
return p.xtaken(len(p.s) - p.o)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) wsp() {
|
func (p *parser) wsp() {
|
||||||
p.xtakefn(func(c rune, i int) bool {
|
p.xtakefn(false, func(c rune, i int) bool {
|
||||||
return c == ' ' || c == '\t'
|
return c == ' ' || c == '\t'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -124,7 +139,7 @@ func (p *parser) take(s string) bool {
|
||||||
|
|
||||||
// ../rfc/6376:657
|
// ../rfc/6376:657
|
||||||
func (p *parser) xtagName() string {
|
func (p *parser) xtagName() string {
|
||||||
return p.xtakefn1(func(c rune, i int) bool {
|
return p.xtakefn1(false, func(c rune, i int) bool {
|
||||||
return isalpha(c) || i > 0 && (isdigit(c) || c == '_')
|
return isalpha(c) || i > 0 && (isdigit(c) || c == '_')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -134,9 +149,9 @@ func (p *parser) xalgorithm() (string, string) {
|
||||||
xtagx := func(c rune, i int) bool {
|
xtagx := func(c rune, i int) bool {
|
||||||
return isalpha(c) || i > 0 && isdigit(c)
|
return isalpha(c) || i > 0 && isdigit(c)
|
||||||
}
|
}
|
||||||
algk := p.xtakefn1(xtagx)
|
algk := p.xtakefn1(false, xtagx)
|
||||||
p.xtake("-")
|
p.xtake("-")
|
||||||
algv := p.xtakefn1(xtagx)
|
algv := p.xtakefn1(false, xtagx)
|
||||||
return algk, algv
|
return algk, algv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +160,7 @@ func (p *parser) xalgorithm() (string, string) {
|
||||||
// ../rfc/6376:1076
|
// ../rfc/6376:1076
|
||||||
func (p *parser) xbase64() []byte {
|
func (p *parser) xbase64() []byte {
|
||||||
s := ""
|
s := ""
|
||||||
p.xtakefn(func(c rune, i int) bool {
|
p.xtakefn(false, func(c rune, i int) bool {
|
||||||
if isalphadigit(c) || c == '+' || c == '/' || c == '=' {
|
if isalphadigit(c) || c == '+' || c == '/' || c == '=' {
|
||||||
s += string(c)
|
s += string(c)
|
||||||
return true
|
return true
|
||||||
|
@ -185,9 +200,9 @@ func (p *parser) xdomain() dns.Domain {
|
||||||
// todo: add a "lax" mode where underscore is allowed if this is a selector? seen in the wild, but invalid: ../rfc/6376:581 ../rfc/5321:2303
|
// todo: add a "lax" mode where underscore is allowed if this is a selector? seen in the wild, but invalid: ../rfc/6376:581 ../rfc/5321:2303
|
||||||
return isalphadigit(c) || (i > 0 && c == '-' && p.o+1 < len(p.s))
|
return isalphadigit(c) || (i > 0 && c == '-' && p.o+1 < len(p.s))
|
||||||
}
|
}
|
||||||
s := p.xtakefn1(subdomain)
|
s := p.xtakefn1(false, subdomain)
|
||||||
for p.hasPrefix(".") {
|
for p.hasPrefix(".") {
|
||||||
s += p.xtake(".") + p.xtakefn1(subdomain)
|
s += p.xtake(".") + p.xtakefn1(false, subdomain)
|
||||||
}
|
}
|
||||||
d, err := dns.ParseDomain(s)
|
d, err := dns.ParseDomain(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -196,23 +211,24 @@ func (p *parser) xdomain() dns.Domain {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) xhdrName() string {
|
func (p *parser) xhdrName(ignoreFWS bool) string {
|
||||||
// ../rfc/6376:473
|
// ../rfc/6376:473
|
||||||
// ../rfc/5322:1689
|
// ../rfc/5322:1689
|
||||||
// BNF for hdr-name (field-name) allows ";", but DKIM disallows unencoded semicolons. ../rfc/6376:643
|
// BNF for hdr-name (field-name) allows ";", but DKIM disallows unencoded semicolons. ../rfc/6376:643
|
||||||
return p.xtakefn1(func(c rune, i int) bool {
|
// ignoreFWS is needed for "z=", which can have FWS anywhere. ../rfc/6376:1372
|
||||||
|
return p.xtakefn1(ignoreFWS, func(c rune, i int) bool {
|
||||||
return c > ' ' && c < 0x7f && c != ':' && c != ';'
|
return c > ' ' && c < 0x7f && c != ':' && c != ';'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) xsignedHeaderFields() []string {
|
func (p *parser) xsignedHeaderFields() []string {
|
||||||
// ../rfc/6376:1157
|
// ../rfc/6376:1157
|
||||||
l := []string{p.xhdrName()}
|
l := []string{p.xhdrName(false)}
|
||||||
for p.peekfws(":") {
|
for p.peekfws(":") {
|
||||||
p.fws()
|
p.fws()
|
||||||
p.xtake(":")
|
p.xtake(":")
|
||||||
p.fws()
|
p.fws()
|
||||||
l = append(l, p.xhdrName())
|
l = append(l, p.xhdrName(false))
|
||||||
}
|
}
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
@ -304,7 +320,7 @@ func (p *parser) xchar() rune {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) xatom() string {
|
func (p *parser) xatom() string {
|
||||||
return p.xtakefn1(func(c rune, i int) bool {
|
return p.xtakefn1(false, func(c rune, i int) bool {
|
||||||
switch c {
|
switch c {
|
||||||
case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~':
|
case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~':
|
||||||
return true
|
return true
|
||||||
|
@ -362,7 +378,7 @@ func (p *parser) xqtagmethod() string {
|
||||||
if strings.EqualFold(s, "dns") && len(rem) >= len("/txt") && strings.EqualFold(rem[:len("/txt")], "/txt") {
|
if strings.EqualFold(s, "dns") && len(rem) >= len("/txt") && strings.EqualFold(rem[:len("/txt")], "/txt") {
|
||||||
s += p.xtaken(4)
|
s += p.xtaken(4)
|
||||||
} else if p.take("/") {
|
} else if p.take("/") {
|
||||||
s += "/" + p.xqp(true, true)
|
s += "/" + p.xqp(true, true, false)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
@ -381,25 +397,27 @@ func isalphadigit(c rune) bool {
|
||||||
|
|
||||||
// ../rfc/6376:469
|
// ../rfc/6376:469
|
||||||
func (p *parser) xhyphenatedWord() string {
|
func (p *parser) xhyphenatedWord() string {
|
||||||
return p.xtakefn1(func(c rune, i int) bool {
|
return p.xtakefn1(false, func(c rune, i int) bool {
|
||||||
return isalpha(c) || i > 0 && isdigit(c) || i > 0 && c == '-' && p.o+i+1 < len(p.s) && isalphadigit(rune(p.s[p.o+i+1]))
|
return isalpha(c) || i > 0 && isdigit(c) || i > 0 && c == '-' && p.o+i+1 < len(p.s) && isalphadigit(rune(p.s[p.o+i+1]))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ../rfc/6376:474
|
// ../rfc/6376:474
|
||||||
func (p *parser) xqphdrvalue() string {
|
func (p *parser) xqphdrvalue(ignoreFWS bool) string {
|
||||||
return p.xqp(true, false)
|
return p.xqp(true, false, ignoreFWS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) xqpSection() string {
|
func (p *parser) xqpSection() string {
|
||||||
return p.xqp(false, false)
|
return p.xqp(false, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dkim-quoted-printable (pipeEncoded true) or qp-section.
|
// dkim-quoted-printable (pipeEncoded true) or qp-section.
|
||||||
//
|
//
|
||||||
// It is described in terms of (lots of) modifications to MIME quoted-printable,
|
// It is described in terms of (lots of) modifications to MIME quoted-printable,
|
||||||
// but it may be simpler to just ignore that reference.
|
// but it may be simpler to just ignore that reference.
|
||||||
func (p *parser) xqp(pipeEncoded, colonEncoded bool) string {
|
//
|
||||||
|
// ignoreFWS is required for "z=", which can have FWS anywhere.
|
||||||
|
func (p *parser) xqp(pipeEncoded, colonEncoded, ignoreFWS bool) string {
|
||||||
// ../rfc/6376:494 ../rfc/2045:1260
|
// ../rfc/6376:494 ../rfc/2045:1260
|
||||||
|
|
||||||
hex := func(c byte) rune {
|
hex := func(c byte) rune {
|
||||||
|
@ -418,12 +436,8 @@ func (p *parser) xqp(pipeEncoded, colonEncoded bool) string {
|
||||||
if colonEncoded && p.hasPrefix(":") {
|
if colonEncoded && p.hasPrefix(":") {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if p.hasPrefix("=") {
|
if p.take("=") {
|
||||||
p.xtake("=")
|
h := p.xtakefn(ignoreFWS, func(c rune, i int) bool {
|
||||||
// note: \r\n before the full hex-octet has been encountered in the wild. Could be
|
|
||||||
// a sender just wrapping their headers after escaping, or not escaping an "=". We
|
|
||||||
// currently don't compensate for it.
|
|
||||||
h := p.xtakefn(func(c rune, i int) bool {
|
|
||||||
return i < 2 && (c >= '0' && c <= '9' || c >= 'A' && c <= 'Z')
|
return i < 2 && (c >= '0' && c <= '9' || c >= 'A' && c <= 'Z')
|
||||||
})
|
})
|
||||||
if len(h) != 2 {
|
if len(h) != 2 {
|
||||||
|
@ -433,7 +447,7 @@ func (p *parser) xqp(pipeEncoded, colonEncoded bool) string {
|
||||||
s += string(c)
|
s += string(c)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
x := p.xtakefn(func(c rune, i int) bool {
|
x := p.xtakefn(ignoreFWS, func(c rune, i int) bool {
|
||||||
return c > ' ' && c < 0x7f && c != ';' && c != '=' && !(pipeEncoded && c == '|')
|
return c > ' ' && c < 0x7f && c != ';' && c != '=' && !(pipeEncoded && c == '|')
|
||||||
})
|
})
|
||||||
if x == "" {
|
if x == "" {
|
||||||
|
@ -465,10 +479,11 @@ func (p *parser) xcopiedHeaderFields() []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) xztagcopy() string {
|
func (p *parser) xztagcopy() string {
|
||||||
// ../rfc/6376:1386
|
// ABNF does not mention FWS (unlike for other fields), but FWS is allowed everywhere in the value...
|
||||||
f := p.xhdrName()
|
// ../rfc/6376:1386 ../rfc/6376:1372
|
||||||
|
f := p.xhdrName(true)
|
||||||
p.fws()
|
p.fws()
|
||||||
p.xtake(":")
|
p.xtake(":")
|
||||||
v := p.xqphdrvalue()
|
v := p.xqphdrvalue(true)
|
||||||
return f + ":" + v
|
return f + ":" + v
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,7 +213,7 @@ func parseSignature(buf []byte, smtputf8 bool) (sig *Sig, verifySig []byte, err
|
||||||
ds := newSigWithDefaults()
|
ds := newSigWithDefaults()
|
||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
p := parser{s: string(buf), smtputf8: smtputf8}
|
p := parser{s: string(buf), smtputf8: smtputf8}
|
||||||
name := p.xhdrName()
|
name := p.xhdrName(false)
|
||||||
if !strings.EqualFold(name, "DKIM-Signature") {
|
if !strings.EqualFold(name, "DKIM-Signature") {
|
||||||
xerrorf("%w", errSigHeader)
|
xerrorf("%w", errSigHeader)
|
||||||
}
|
}
|
||||||
|
|
24
main.go
24
main.go
|
@ -1123,13 +1123,25 @@ that was passed.
|
||||||
xcheckf(err, "dkim verify")
|
xcheckf(err, "dkim verify")
|
||||||
|
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
record, err := result.Record.Record()
|
var sigh string
|
||||||
if err != nil {
|
if result.Sig == nil {
|
||||||
log.Printf("warning: record: %s", err)
|
log.Printf("warning: could not parse signature")
|
||||||
|
} else {
|
||||||
|
sigh, err = result.Sig.Header()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("warning: packing signature: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sigh, err := result.Sig.Header()
|
var txt string
|
||||||
xcheckf(err, "packing dkim-signature header")
|
if result.Record == nil {
|
||||||
fmt.Printf("status %q, err %v\nrecord %s\nheader %s\n", result.Status, result.Err, record, sigh)
|
log.Printf("warning: missing DNS record")
|
||||||
|
} else {
|
||||||
|
txt, err = result.Record.Record()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("warning: packing record: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue