diff --git a/dkim/dkim.go b/dkim/dkim.go index 8a4b59b..c176702 100644 --- a/dkim/dkim.go +++ b/dkim/dkim.go @@ -2,9 +2,241 @@ package dkim import ( + "bytes" + "strings" "time" ) var now = time.Now const headerFieldName = "DKIM-Signature" + +type DKIMTag interface { + Reset() + GetString(limit int) (chars string, ok bool) + GetRemaining() string + Done() bool +} + +type DKIMTagBase64 struct { + TagLen int + TagAndValue string + Idx int +} + +func (t *DKIMTagBase64) Reset() { + t.Idx = 0 +} + +func (t *DKIMTagBase64) NextBreak(idx int, max int) int { + if idx == 0 { + return t.TagLen + } else if idx == t.TagLen { + return t.TagLen+1 + } else { + end := len(t.TagAndValue) + if max < end { + return max + } else { + return end + } + } +} + +func (t *DKIMTagBase64) GetString(limit int) (chars string, ok bool) { + end_max := len(t.TagAndValue) + if end_max - t.Idx <= limit { + chars = t.TagAndValue[t.Idx:] + t.Idx = end_max + ok = true + return + } + if t.Idx + limit < end_max { + end_max = t.Idx + limit + } + end := t.Idx + for end < end_max { + idx := t.NextBreak(end, end_max) + if idx <= end_max { + end = idx + } else { + break + } + } + if t.Idx < end { + chars = t.TagAndValue[t.Idx:end] + t.Idx = end + ok = true + } + return +} + +func (t *DKIMTagBase64) GetRemaining() string { + chars := t.TagAndValue[t.Idx:] + t.Idx = len(t.TagAndValue) + return chars +} + +func (t *DKIMTagBase64) Done() bool { + return t.Idx == len(t.TagAndValue) +} + +type DKIMTagDelim struct { + TagLen int + TagAndValue string + Delimiter string + Idx int +} + +func (t *DKIMTagDelim) Reset() { + t.Idx = 0 +} + +func (t *DKIMTagDelim) NextBreak(idx int) int { + if idx == 0 { + return t.TagLen + } else if idx == t.TagLen { + return t.TagLen+1 + } else { + if t.Delimiter == "" { + return len(t.TagAndValue) + } else { + i := strings.Index(t.TagAndValue[idx:], t.Delimiter) + if i == -1 { + return len(t.TagAndValue) + } else { + if i == 0 { + return idx + len(t.Delimiter) + } else { + return i + idx + } + } + } + } +} + +func (t *DKIMTagDelim) GetString(limit int) (chars string, ok bool) { + end_max := len(t.TagAndValue) + if end_max - t.Idx <= limit { + chars = t.TagAndValue[t.Idx:] + t.Idx = end_max + ok = true + return + } + if t.Idx + limit < end_max { + end_max = t.Idx + limit + } + end := t.Idx + for end < end_max { + idx := t.NextBreak(end) + if idx <= end_max { + end = idx + } else { + break + } + } + if t.Idx < end { + chars = t.TagAndValue[t.Idx:end] + t.Idx = end + ok = true + } + return +} + +func (t *DKIMTagDelim) GetRemaining() string { + chars := t.TagAndValue[t.Idx:] + t.Idx = len(t.TagAndValue) + return chars +} + +func (t *DKIMTagDelim) Done() bool { + return t.Idx == len(t.TagAndValue) +} + +func NewDKIMTagPlain(tag string, value string) DKIMTag { + dtag := &DKIMTagDelim{ + TagLen: len(tag), + TagAndValue: tag+"="+value, + } + return dtag +} + +func NewDKIMTagDelim(tag string, values []string, delimiter string) DKIMTag { + var sbuf bytes.Buffer + sbuf.WriteString(tag) + sbuf.WriteString("=") + for idx, value := range values { + if idx > 0 { + sbuf.WriteString(delimiter) + } + sbuf.WriteString(value) + } + dtag := &DKIMTagDelim{ + TagLen: len(tag), + TagAndValue: sbuf.String(), + Delimiter: delimiter, + } + return dtag +} + +func NewDKIMTagBase64(tag string, value string) DKIMTag { + dtag := &DKIMTagBase64{ + TagLen: len(tag), + TagAndValue: tag+"="+value, + } + return dtag +} + +type DKIMSignature struct { + Buf bytes.Buffer + LineLen int +} + +func (sig *DKIMSignature) AddTag(tag DKIMTag) { + tag.Reset() + for ! tag.Done() { + max_chars := 80 - sig.LineLen - 1 - 2 // allow for CRLF and also the semi-colon + if max_chars <= 0 { + sig.Buf.WriteString("\r\n ") + sig.LineLen = 1 + continue + } + s, ok := tag.GetString(max_chars) + if !ok { + if sig.LineLen > 1 { + sig.Buf.WriteString("\r\n ") + sig.LineLen = 1 + continue + } else { + // we can't break the line, we are forced to just put it in + s = tag.GetRemaining() + } + } + sig.Buf.WriteString(s) + sig.LineLen += len(s) + if tag.Done() { + sig.Buf.WriteString(";") + sig.LineLen += 1 + } + } +} + +func (sig *DKIMSignature) AddPlainTag(tag string, value string) { + sig.AddTag(NewDKIMTagPlain(tag, value)) +} + +func (sig *DKIMSignature) AddDelimTag(tag string, values []string, delimiter string) { + sig.AddTag(NewDKIMTagDelim(tag, values, delimiter)) +} + +func (sig *DKIMSignature) AddBase64Tag(tag string, value string) { + sig.AddTag(NewDKIMTagBase64(tag, value)) +} + +func NewDKIMSignature() *DKIMSignature { + sig := DKIMSignature{} + sig.Buf.WriteString(headerFieldName) + sig.Buf.WriteString(": ") + sig.LineLen = len(headerFieldName)+2 + return &sig +} diff --git a/dkim/dkim_signature_test.go b/dkim/dkim_signature_test.go new file mode 100644 index 0000000..5ef7bc9 --- /dev/null +++ b/dkim/dkim_signature_test.go @@ -0,0 +1,152 @@ +package dkim + +import "testing" + +func TestNewDKIMTagPlain(t *testing.T) { + t1 := NewDKIMTagPlain("a", "123456") + if t1.Done() { + t.Errorf("tag Done after init") + } + s := t1.GetRemaining() + if s != "a=123456" { + t.Errorf("GetRemaining failed after init") + } + if ! t1.Done() { + t.Errorf("!Done after GetRemaining") + } + t1.Reset() + var ok bool + s, ok = t1.GetString(3) + if !ok { + t.Errorf("failed to break after =") + } + if s != "a=" { + t.Errorf("failed to get name=") + } + s, ok = t1.GetString(8) + if ! ok { + t.Errorf("!ok for entire values") + } + if s != "123456" { + t.Errorf("s bad for GetString") + } + if ! t1.Done() { + t.Errorf("!Done after GetString for all") + } + t1.Reset() + s, ok = t1.GetString(2) + if !ok { + t.Errorf("GetString = failed") + } + if s != "a=" { + t.Errorf("GetString = resulted in %v", s) + } + if t1.Done() { + t.Errorf("Done after partial") + } + s, ok = t1.GetString(3) + if ok { + t.Errorf("GetString returned partial value") + } + s, ok = t1.GetString(6) + if !ok { + t.Errorf("GetString did not return value") + } + if s != "123456" { + t.Errorf("GetString returned wrong partial %v", s) + } + if ! t1.Done() { + t.Errorf("!Done after getting last bit") + } + t1.Reset() + s, ok = t1.GetString(1) + if ! ok { + t.Errorf("GetString failed") + } + if s != "a" { + t.Errorf("GetString incorrect %v", s) + } + s, ok = t1.GetString(4) + if !ok { + t.Errorf("GetString failed getting partial") + } + if s != "=" { + t.Errorf("GetString != =") + } + s, ok = t1.GetString(6) + if !ok { + t.Errorf("GetString remaining failed") + } + if s != "123456" { + t.Errorf("GetString remaining failed: %v", s) + } + if ! t1.Done() { + t.Errorf("Not done after partial") + } + + t2 := NewDKIMTagPlain("ab", "123456") + s, ok = t2.GetString(1) + if ok { + t.Errorf("incorrectly got part of name: %v", s) + } +} + +func TestNewDKIMTagDelim(t *testing.T) { + dt := NewDKIMTagDelim("h", []string{"To", "From", "Subject", "Date", "Message-ID", + "MIME-Version", "Content-Type", "Content-Transfer-Encoding"}, ":") + s, ok := dt.GetString(1) + if !ok { + t.Errorf("failed to get tag-name") + } + if s != "h" { + t.Errorf("tag-name is wrong: %v", s) + } + s, ok = dt.GetString(2) + if !ok { + t.Errorf("failed to get =") + } + if s != "=" { + t.Errorf("'=' != %v", s) + } + s, ok = dt.GetString(2) + if !ok { + t.Errorf("did not get To") + } + if s != "To" { + t.Errorf("'%v' != To", s) + } + s, ok = dt.GetString(2) + if !ok { + t.Errorf("failed to get ;") + } + if s != ":" { + t.Errorf("':' != %v", s) + } +} + +func TestNewDKIMTagBase64(t *testing.T) { + dt := NewDKIMTagBase64("bh", "7Xgui0yFAxLMluvjaRLRKJPgrOpPtHSIYy/BndZ2zLg=") + s, ok := dt.GetString(1) + if ok { + t.Errorf("got partial tag-name") + } + s, ok = dt.GetString(3) + if !ok { + t.Errorf("failed to get name=") + return + } + if s != "bh=" { + t.Errorf("'%v' != bh=", s) + } + s, ok = dt.GetString(5) + if !ok { + t.Errorf("failed to get b64 data") + } + if s != "7Xgui" { + t.Errorf("'%v' != 7Xgui", s) + } + s = dt.GetRemaining() + if s != "0yFAxLMluvjaRLRKJPgrOpPtHSIYy/BndZ2zLg=" { + t.Errorf("remaining is incorrect") + } +} \ No newline at end of file diff --git a/dkim/sign.go b/dkim/sign.go index 843ce18..a5da198 100644 --- a/dkim/sign.go +++ b/dkim/sign.go @@ -80,9 +80,9 @@ type SignOptions struct { // After a successful Close, Signature can be called to retrieve the // DKIM-Signature header field that the caller should prepend to the message. type Signer struct { - pw *io.PipeWriter - done <-chan error - sigParams map[string]string // only valid after done received nil + pw *io.PipeWriter + done <-chan error + dkimSig string // only valid after done received nil } // NewSigner creates a new signer. It returns an error if SignOptions is @@ -191,17 +191,14 @@ func NewSigner(options *SignOptions) (*Signer, error) { } bodyHashed := hasher.Sum(nil) - params := map[string]string{ - "v": "1", - "a": keyAlgo + "-" + hashAlgo, - "bh": base64.StdEncoding.EncodeToString(bodyHashed), - "c": string(headerCan) + "/" + string(bodyCan), - "d": options.Domain, - //"l": "", // TODO - "s": options.Selector, - "t": formatTime(now()), - //"z": "", // TODO - } + dkim_sig := NewDKIMSignature() + dkim_sig.AddPlainTag("v", "1") + dkim_sig.AddPlainTag("a", keyAlgo + "-" + hashAlgo) + dkim_sig.AddBase64Tag("bh", base64.StdEncoding.EncodeToString(bodyHashed)) + dkim_sig.AddPlainTag("c", string(headerCan) + "/" + string(bodyCan)) + dkim_sig.AddPlainTag("d", options.Domain) + dkim_sig.AddPlainTag("s", options.Selector) + dkim_sig.AddPlainTag("t", formatTime(now())) var headerKeys []string if options.HeaderKeys != nil { @@ -212,10 +209,10 @@ func NewSigner(options *SignOptions) (*Signer, error) { headerKeys = append(headerKeys, k) } } - params["h"] = formatTagList(headerKeys) + dkim_sig.AddDelimTag("h", headerKeys, ":") if options.Identifier != "" { - params["i"] = options.Identifier + dkim_sig.AddPlainTag("i", options.Identifier) } if options.QueryMethods != nil { @@ -223,11 +220,11 @@ func NewSigner(options *SignOptions) (*Signer, error) { for i, method := range options.QueryMethods { methods[i] = string(method) } - params["q"] = formatTagList(methods) + dkim_sig.AddDelimTag("q", methods, ":") } if !options.Expiration.IsZero() { - params["x"] = formatTime(options.Expiration) + dkim_sig.AddPlainTag("x", formatTime(options.Expiration)) } // Hash and sign headers @@ -250,8 +247,10 @@ func NewSigner(options *SignOptions) (*Signer, error) { } } - params["b"] = "" - sigField := formatSignature(params) + sig_len := dkim_sig.Buf.Len() + sig_line_len := dkim_sig.LineLen + dkim_sig.AddBase64Tag("b", "") + sigField := dkim_sig.Buf.String() sigField = canonicalizers[headerCan].CanonicalizeHeader(sigField) sigField = strings.TrimRight(sigField, crlf) if _, err := io.WriteString(hasher, sigField); err != nil { @@ -271,9 +270,11 @@ func NewSigner(options *SignOptions) (*Signer, error) { closeReadWithError(err) return } - params["b"] = base64.StdEncoding.EncodeToString(sig) + dkim_sig.Buf.Truncate(sig_len) + dkim_sig.LineLen = sig_line_len + dkim_sig.AddBase64Tag("b", base64.StdEncoding.EncodeToString(sig)) - s.sigParams = params + s.dkimSig = dkim_sig.Buf.String() closeReadWithError(nil) }() @@ -299,10 +300,10 @@ func (s *Signer) Close() error { // The returned value contains both the header field name, its value and the // final CRLF. func (s *Signer) Signature() string { - if s.sigParams == nil { + if s.dkimSig == "" { panic("dkim: Signer.Signature must only be called after a succesful Signer.Close") } - return formatSignature(s.sigParams) + return s.dkimSig + crlf } // Sign signs a message. It reads it from r and writes the signed version to w. diff --git a/go.sum b/go.sum index 12693da..1c67576 100644 --- a/go.sum +++ b/go.sum @@ -2,4 +2,5 @@ github.com/emersion/go-milter v0.0.0-20190311184326-c3095a41a6fe h1:nIpzZ2zruw1M github.com/emersion/go-milter v0.0.0-20190311184326-c3095a41a6fe/go.mod h1:aEaq7U51ARlk+2UeXTtdrDYeYWAUn/QjEwWzs7lD8OU= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=