1// Package dkim provides tools for signing and verify a email according to RFC 6376 2package dkim 3 4import ( 5 "bytes" 6 "container/list" 7 "crypto" 8 "crypto/rand" 9 "crypto/rsa" 10 "crypto/sha1" 11 "crypto/sha256" 12 "crypto/x509" 13 "encoding/base64" 14 "encoding/pem" 15 "hash" 16 "regexp" 17 "strings" 18 "time" 19) 20 21const ( 22 CRLF = "\r\n" 23 TAB = " " 24 FWS = CRLF + TAB 25 MaxHeaderLineLength = 70 26) 27 28type verifyOutput int 29 30const ( 31 SUCCESS verifyOutput = 1 + iota 32 PERMFAIL 33 TEMPFAIL 34 NOTSIGNED 35 TESTINGSUCCESS 36 TESTINGPERMFAIL 37 TESTINGTEMPFAIL 38) 39 40// sigOptions represents signing options 41type SigOptions struct { 42 43 // DKIM version (default 1) 44 Version uint 45 46 // Private key used for signing (required) 47 PrivateKey []byte 48 49 // Domain (required) 50 Domain string 51 52 // Selector (required) 53 Selector string 54 55 // The Agent of User IDentifier 56 Auid string 57 58 // Message canonicalization (plain-text; OPTIONAL, default is 59 // "simple/simple"). This tag informs the Verifier of the type of 60 // canonicalization used to prepare the message for signing. 61 Canonicalization string 62 63 // The algorithm used to generate the signature 64 //"rsa-sha1" or "rsa-sha256" 65 Algo string 66 67 // Signed header fields 68 Headers []string 69 70 // Body length count( if set to 0 this tag is ommited in Dkim header) 71 BodyLength uint 72 73 // Query Methods used to retrieve the public key 74 QueryMethods []string 75 76 // Add a signature timestamp 77 AddSignatureTimestamp bool 78 79 // Time validity of the signature (0=never) 80 SignatureExpireIn uint64 81 82 // CopiedHeaderFileds 83 CopiedHeaderFields []string 84} 85 86// NewSigOptions returns new sigoption with some defaults value 87func NewSigOptions() SigOptions { 88 return SigOptions{ 89 Version: 1, 90 Canonicalization: "simple/simple", 91 Algo: "rsa-sha256", 92 Headers: []string{"from"}, 93 BodyLength: 0, 94 QueryMethods: []string{"dns/txt"}, 95 AddSignatureTimestamp: true, 96 SignatureExpireIn: 0, 97 } 98} 99 100// Sign signs an email 101func Sign(email *[]byte, options SigOptions) error { 102 var privateKey *rsa.PrivateKey 103 var err error 104 105 // PrivateKey 106 if len(options.PrivateKey) == 0 { 107 return ErrSignPrivateKeyRequired 108 } 109 d, _ := pem.Decode(options.PrivateKey) 110 if d == nil { 111 return ErrCandNotParsePrivateKey 112 } 113 114 // try to parse it as PKCS1 otherwise try PKCS8 115 if key, err := x509.ParsePKCS1PrivateKey(d.Bytes); err != nil { 116 if key, err := x509.ParsePKCS8PrivateKey(d.Bytes); err != nil { 117 return ErrCandNotParsePrivateKey 118 } else { 119 privateKey = key.(*rsa.PrivateKey) 120 } 121 } else { 122 privateKey = key 123 } 124 125 // Domain required 126 if options.Domain == "" { 127 return ErrSignDomainRequired 128 } 129 130 // Selector required 131 if options.Selector == "" { 132 return ErrSignSelectorRequired 133 } 134 135 // Canonicalization 136 options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization)) 137 if err != nil { 138 return err 139 } 140 141 // Algo 142 options.Algo = strings.ToLower(options.Algo) 143 if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" { 144 return ErrSignBadAlgo 145 } 146 147 // Header must contain "from" 148 hasFrom := false 149 for i, h := range options.Headers { 150 h = strings.ToLower(h) 151 options.Headers[i] = h 152 if h == "from" { 153 hasFrom = true 154 } 155 } 156 if !hasFrom { 157 return ErrSignHeaderShouldContainsFrom 158 } 159 160 // Normalize 161 headers, body, err := canonicalize(email, options.Canonicalization, options.Headers) 162 if err != nil { 163 return err 164 } 165 166 signHash := strings.Split(options.Algo, "-") 167 168 // hash body 169 bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength) 170 if err != nil { 171 return err 172 } 173 174 // Get dkim header base 175 dkimHeader := newDkimHeaderBySigOptions(options) 176 dHeader := dkimHeader.getHeaderBaseForSigning(bodyHash) 177 178 canonicalizations := strings.Split(options.Canonicalization, "/") 179 dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) 180 if err != nil { 181 return err 182 } 183 headers = append(headers, []byte(dHeaderCanonicalized)...) 184 headers = bytes.TrimRight(headers, " \r\n") 185 186 // sign 187 sig, err := getSignature(&headers, privateKey, signHash[1]) 188 189 // add to DKIM-Header 190 subh := "" 191 l := len(subh) 192 for _, c := range sig { 193 subh += string(c) 194 l++ 195 if l >= MaxHeaderLineLength { 196 dHeader += subh + FWS 197 subh = "" 198 l = 0 199 } 200 } 201 dHeader += subh + CRLF 202 *email = append([]byte(dHeader), *email...) 203 return nil 204} 205 206// Verify verifies an email an return 207// state: SUCCESS or PERMFAIL or TEMPFAIL, TESTINGSUCCESS, TESTINGPERMFAIL 208// TESTINGTEMPFAIL or NOTSIGNED 209// error: if an error occurs during verification 210func Verify(email *[]byte, opts ...DNSOpt) (verifyOutput, error) { 211 // parse email 212 dkimHeader, err := GetHeader(email) 213 if err != nil { 214 if err == ErrDkimHeaderNotFound { 215 return NOTSIGNED, ErrDkimHeaderNotFound 216 } 217 return PERMFAIL, err 218 } 219 220 // we do not set query method because if it's others, validation failed earlier 221 pubKey, verifyOutputOnError, err := NewPubKeyRespFromDNS(dkimHeader.Selector, dkimHeader.Domain, opts...) 222 if err != nil { 223 // fix https://github.com/toorop/go-dkim/issues/1 224 //return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting) 225 return verifyOutputOnError, err 226 } 227 228 // Normalize 229 headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) 230 if err != nil { 231 return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) 232 } 233 sigHash := strings.Split(dkimHeader.Algorithm, "-") 234 // check if hash algo are compatible 235 compatible := false 236 for _, algo := range pubKey.HashAlgo { 237 if sigHash[1] == algo { 238 compatible = true 239 break 240 } 241 } 242 if !compatible { 243 return getVerifyOutput(PERMFAIL, ErrVerifyInappropriateHashAlgo, pubKey.FlagTesting) 244 } 245 246 // expired ? 247 if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Second() < time.Now().Second() { 248 return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting) 249 250 } 251 252 //println("|" + string(body) + "|") 253 // get body hash 254 bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength) 255 if err != nil { 256 return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) 257 } 258 //println(bodyHash) 259 if bodyHash != dkimHeader.BodyHash { 260 return getVerifyOutput(PERMFAIL, ErrVerifyBodyHash, pubKey.FlagTesting) 261 } 262 263 // compute sig 264 dkimHeaderCano, err := canonicalizeHeader(dkimHeader.rawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0]) 265 if err != nil { 266 return getVerifyOutput(TEMPFAIL, err, pubKey.FlagTesting) 267 } 268 toSignStr := string(headers) + dkimHeaderCano 269 toSign := bytes.TrimRight([]byte(toSignStr), " \r\n") 270 271 err = verifySignature(toSign, dkimHeader.SignatureData, &pubKey.PubKey, sigHash[1]) 272 if err != nil { 273 return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) 274 } 275 return SUCCESS, nil 276} 277 278// getVerifyOutput returns output of verify fct according to the testing flag 279func getVerifyOutput(status verifyOutput, err error, flagTesting bool) (verifyOutput, error) { 280 if !flagTesting { 281 return status, err 282 } 283 switch status { 284 case SUCCESS: 285 return TESTINGSUCCESS, err 286 case PERMFAIL: 287 return TESTINGPERMFAIL, err 288 case TEMPFAIL: 289 return TESTINGTEMPFAIL, err 290 } 291 // should never happen but compilator sream whithout return 292 return status, err 293} 294 295// canonicalize returns canonicalized version of header and body 296func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, err error) { 297 body = []byte{} 298 rxReduceWS := regexp.MustCompile(`[ \t]+`) 299 300 rawHeaders, rawBody, err := getHeadersBody(email) 301 if err != nil { 302 return nil, nil, err 303 } 304 305 canonicalizations := strings.Split(cano, "/") 306 307 // canonicalyze header 308 headersList, err := getHeadersList(&rawHeaders) 309 310 // pour chaque header a conserver on traverse tous les headers dispo 311 // If multi instance of a field we must keep it from the bottom to the top 312 var match *list.Element 313 headersToKeepList := list.New() 314 315 for _, headerToKeep := range h { 316 match = nil 317 headerToKeepToLower := strings.ToLower(headerToKeep) 318 for e := headersList.Front(); e != nil; e = e.Next() { 319 //fmt.Printf("|%s|\n", e.Value.(string)) 320 t := strings.Split(e.Value.(string), ":") 321 if strings.ToLower(t[0]) == headerToKeepToLower { 322 match = e 323 } 324 } 325 if match != nil { 326 headersToKeepList.PushBack(match.Value.(string) + "\r\n") 327 headersList.Remove(match) 328 } 329 } 330 331 //if canonicalizations[0] == "simple" { 332 for e := headersToKeepList.Front(); e != nil; e = e.Next() { 333 cHeader, err := canonicalizeHeader(e.Value.(string), canonicalizations[0]) 334 if err != nil { 335 return headers, body, err 336 } 337 headers = append(headers, []byte(cHeader)...) 338 } 339 // canonicalyze body 340 if canonicalizations[1] == "simple" { 341 // simple 342 // The "simple" body canonicalization algorithm ignores all empty lines 343 // at the end of the message body. An empty line is a line of zero 344 // length after removal of the line terminator. If there is no body or 345 // no trailing CRLF on the message body, a CRLF is added. It makes no 346 // other changes to the message body. In more formal terms, the 347 // "simple" body canonicalization algorithm converts "*CRLF" at the end 348 // of the body to a single "CRLF". 349 // Note that a completely empty or missing body is canonicalized as a 350 // single "CRLF"; that is, the canonicalized length will be 2 octets. 351 body = bytes.TrimRight(rawBody, "\r\n") 352 body = append(body, []byte{13, 10}...) 353 } else { 354 // relaxed 355 // Ignore all whitespace at the end of lines. Implementations 356 // MUST NOT remove the CRLF at the end of the line. 357 // Reduce all sequences of WSP within a line to a single SP 358 // character. 359 // Ignore all empty lines at the end of the message body. "Empty 360 // line" is defined in Section 3.4.3. If the body is non-empty but 361 // does not end with a CRLF, a CRLF is added. (For email, this is 362 // only possible when using extensions to SMTP or non-SMTP transport 363 // mechanisms.) 364 rawBody = rxReduceWS.ReplaceAll(rawBody, []byte(" ")) 365 for _, line := range bytes.SplitAfter(rawBody, []byte{10}) { 366 line = bytes.TrimRight(line, " \r\n") 367 body = append(body, line...) 368 body = append(body, []byte{13, 10}...) 369 } 370 body = bytes.TrimRight(body, "\r\n") 371 body = append(body, []byte{13, 10}...) 372 373 } 374 return 375} 376 377// canonicalizeHeader returns canonicalized version of header 378func canonicalizeHeader(header string, algo string) (string, error) { 379 //rxReduceWS := regexp.MustCompile(`[ \t]+`) 380 if algo == "simple" { 381 // The "simple" header canonicalization algorithm does not change header 382 // fields in any way. Header fields MUST be presented to the signing or 383 // verification algorithm exactly as they are in the message being 384 // signed or verified. In particular, header field names MUST NOT be 385 // case folded and whitespace MUST NOT be changed. 386 return header, nil 387 } else if algo == "relaxed" { 388 // The "relaxed" header canonicalization algorithm MUST apply the 389 // following steps in order: 390 391 // Convert all header field names (not the header field values) to 392 // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC". 393 394 // Unfold all header field continuation lines as described in 395 // [RFC5322]; in particular, lines with terminators embedded in 396 // continued header field values (that is, CRLF sequences followed by 397 // WSP) MUST be interpreted without the CRLF. Implementations MUST 398 // NOT remove the CRLF at the end of the header field value. 399 400 // Convert all sequences of one or more WSP characters to a single SP 401 // character. WSP characters here include those before and after a 402 // line folding boundary. 403 404 // Delete all WSP characters at the end of each unfolded header field 405 // value. 406 407 // Delete any WSP characters remaining before and after the colon 408 // separating the header field name from the header field value. The 409 // colon separator MUST be retained. 410 kv := strings.SplitN(header, ":", 2) 411 if len(kv) != 2 { 412 return header, ErrBadMailFormatHeaders 413 } 414 k := strings.ToLower(kv[0]) 415 k = strings.TrimSpace(k) 416 v := removeFWS(kv[1]) 417 //v = rxReduceWS.ReplaceAllString(v, " ") 418 //v = strings.TrimSpace(v) 419 return k + ":" + v + CRLF, nil 420 } 421 return header, ErrSignBadCanonicalization 422} 423 424// getBodyHash return the hash (bas64encoded) of the body 425func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) { 426 var h hash.Hash 427 if algo == "sha1" { 428 h = sha1.New() 429 } else { 430 h = sha256.New() 431 } 432 toH := *body 433 // if l tag (body length) 434 if bodyLength != 0 { 435 if uint(len(toH)) < bodyLength { 436 return "", ErrBadDKimTagLBodyTooShort 437 } 438 toH = toH[0:bodyLength] 439 } 440 441 h.Write(toH) 442 return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil 443} 444 445// getSignature return signature of toSign using key 446func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, error) { 447 var h1 hash.Hash 448 var h2 crypto.Hash 449 switch algo { 450 case "sha1": 451 h1 = sha1.New() 452 h2 = crypto.SHA1 453 break 454 case "sha256": 455 h1 = sha256.New() 456 h2 = crypto.SHA256 457 break 458 default: 459 return "", ErrVerifyInappropriateHashAlgo 460 } 461 462 // sign 463 h1.Write(*toSign) 464 sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil)) 465 if err != nil { 466 return "", err 467 } 468 return base64.StdEncoding.EncodeToString(sig), nil 469} 470 471// verifySignature verify signature from pubkey 472func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo string) error { 473 var h1 hash.Hash 474 var h2 crypto.Hash 475 switch algo { 476 case "sha1": 477 h1 = sha1.New() 478 h2 = crypto.SHA1 479 break 480 case "sha256": 481 h1 = sha256.New() 482 h2 = crypto.SHA256 483 break 484 default: 485 return ErrVerifyInappropriateHashAlgo 486 } 487 488 h1.Write(toSign) 489 sig, err := base64.StdEncoding.DecodeString(sig64) 490 if err != nil { 491 return err 492 } 493 return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig) 494} 495 496// removeFWS removes all FWS from string 497func removeFWS(in string) string { 498 rxReduceWS := regexp.MustCompile(`[ \t]+`) 499 out := strings.Replace(in, "\n", "", -1) 500 out = strings.Replace(out, "\r", "", -1) 501 out = rxReduceWS.ReplaceAllString(out, " ") 502 return strings.TrimSpace(out) 503} 504 505// validateCanonicalization validate canonicalization (c flag) 506func validateCanonicalization(cano string) (string, error) { 507 p := strings.Split(cano, "/") 508 if len(p) > 2 { 509 return "", ErrSignBadCanonicalization 510 } 511 if len(p) == 1 { 512 cano = cano + "/simple" 513 } 514 for _, c := range p { 515 if c != "simple" && c != "relaxed" { 516 return "", ErrSignBadCanonicalization 517 } 518 } 519 return cano, nil 520} 521 522// getHeadersList returns headers as list 523func getHeadersList(rawHeader *[]byte) (*list.List, error) { 524 headersList := list.New() 525 currentHeader := []byte{} 526 for _, line := range bytes.SplitAfter(*rawHeader, []byte{10}) { 527 if line[0] == 32 || line[0] == 9 { 528 if len(currentHeader) == 0 { 529 return headersList, ErrBadMailFormatHeaders 530 } 531 currentHeader = append(currentHeader, line...) 532 } else { 533 // New header, save current if exists 534 if len(currentHeader) != 0 { 535 headersList.PushBack(string(bytes.TrimRight(currentHeader, "\r\n"))) 536 currentHeader = []byte{} 537 } 538 currentHeader = append(currentHeader, line...) 539 } 540 } 541 headersList.PushBack(string(currentHeader)) 542 return headersList, nil 543} 544 545// getHeadersBody return headers and body 546func getHeadersBody(email *[]byte) ([]byte, []byte, error) { 547 substitutedEmail := *email 548 549 // only replace \n with \r\n when \r\n\r\n not exists 550 if bytes.Index(*email, []byte{13, 10, 13, 10}) < 0 { 551 // \n -> \r\n 552 substitutedEmail = bytes.Replace(*email, []byte{10}, []byte{13, 10}, -1) 553 } 554 555 parts := bytes.SplitN(substitutedEmail, []byte{13, 10, 13, 10}, 2) 556 if len(parts) != 2 { 557 return []byte{}, []byte{}, ErrBadMailFormat 558 } 559 // Empty body 560 if len(parts[1]) == 0 { 561 parts[1] = []byte{13, 10} 562 } 563 return parts[0], parts[1], nil 564} 565