Files
go-dkim/dkim.go
Stéphane Depierrepont aka Toorop 3c7c1e2829 body canonicalization
2015-05-04 12:33:01 +02:00

182 lines
4.0 KiB
Go

package dkim
import (
"bytes"
//"fmt"
"io/ioutil"
"regexp"
"strings"
"time"
)
const (
CRLF = "\r\n"
)
// sigOptions represents signing options
type sigOptions struct {
// DKIM version (default 1)
Version uint
// Private key used for signing (required)
PrivateKey string
// Domain (required)
Domain string
// Selector (required)
Selector string
// The Agent of User IDentifier
Auid string
// Message canonicalization (plain-text; OPTIONAL, default is
// "simple/simple"). This tag informs the Verifier of the type of
// canonicalization used to prepare the message for signing.
Canonicalization string
// The algorithm used to generate the signature
//"rsa-sha1" or "rsa-sha256"
Algo string
// Signed header fields
Headers []string
// Body length count( if set to 0 this tag is ommited in Dkim header)
BodyLength uint
// Query Methods used to retrieve the public key
QueryMethods []string
// Add a signature timestamp
AddSignatureTimestamp bool
// Time validity of the signature (0=never)
SignatureExpireIn time.Duration
}
// NewSigOption returns new sigoption with some defaults value
func NewSigOptions() sigOptions {
return sigOptions{
Version: 1,
Canonicalization: "simple/simple",
Algo: "rsa-sha256",
Headers: []string{"from"},
BodyLength: 0,
QueryMethods: []string{"dns/txt"},
AddSignatureTimestamp: false,
SignatureExpireIn: 0 * time.Second,
}
}
// Sign signs an email
func Sign(email *bytes.Reader, options sigOptions) (*bytes.Reader, error) {
// check && sanitize config
// PrivateKey (required & TODO: valid)
if options.PrivateKey == "" {
return nil, ErrSignPrivateKeyRequired
}
// Domain required
if options.Domain == "" {
return nil, ErrSignDomainRequired
}
// Selector required
if options.Selector == "" {
return nil, ErrSignSelectorRequired
}
// Canonicalization
options.Canonicalization = strings.ToLower(options.Canonicalization)
p := strings.Split(options.Canonicalization, "/")
if len(p) > 2 {
return nil, ErrSignBadCanonicalization
}
if len(p) == 1 {
options.Canonicalization = options.Canonicalization + "/simple"
}
for _, c := range p {
if c != "simple" && c != "relaxed" {
return nil, ErrSignBadCanonicalization
}
}
// Algo
options.Algo = strings.ToLower(options.Algo)
if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" {
return nil, ErrSignBadAlgo
}
// Header must contain "from"
// normalize -> strtlower
hasFrom := false
for i, h := range options.Headers {
h = strings.ToLower(h)
options.Headers[i] = h
if h == "from" {
hasFrom = true
}
}
if !hasFrom {
return nil, ErrSignHeaderShouldContainsFrom
}
// Normalize
//normalizedHeaders, NormalizedBody, err := normalize(email, options)
canonicalize(email, options)
return nil, nil
}
func canonicalize(emailReader *bytes.Reader, options sigOptions) (headers, body []byte, err error) {
var email []byte
body = []byte{}
rxReduceWS := regexp.MustCompile(`[ \t]+`)
email, err = ioutil.ReadAll(emailReader)
emailReader.Seek(0, 0)
if err != nil {
return
}
parts := bytes.SplitN(email, []byte{13, 10, 13, 10}, 2)
if len(parts) != 2 {
return headers, body, ErrBadMailFormat
}
// Empty body
if len(parts[1]) == 0 {
parts[1] = []byte{13, 10}
}
canonicalizations := strings.Split(options.Canonicalization, "/")
// canonicalyze body
if canonicalizations[1] == "simple" {
body = bytes.TrimRight(parts[1], "\r\n")
body = append(body, []byte{13, 10}...)
} else {
parts[1] = rxReduceWS.ReplaceAll(parts[1], []byte(" "))
for _, line := range bytes.SplitAfter(parts[1], []byte{10}) {
line = bytes.TrimRight(line, " \r\n")
// Ignore all whitespace at the end of lines. Implementations
// MUST NOT remove the CRLF at the end of the line.
if len(line) != 0 {
body = append(body, line...)
body = append(body, []byte{13, 10}...)
}
}
}
return
/*println(string(parts[0]))
println("\r\n")
println(string(parts[1]))
println(string(body))*/
return
}