├── .gitignore ├── Gomfile ├── handlers.go ├── eml_test.go ├── parse_eml.go ├── lru_handlers.go ├── memcache_handlers.go ├── dns_handlers.go ├── struct.go ├── README.md ├── verify.go ├── test_data ├── valid_1.eml └── invalid_1.eml └── dkim.go /.gitignore: -------------------------------------------------------------------------------- 1 | *swp 2 | _vendor 3 | *.prof 4 | reports/* 5 | .DS_Store 6 | .idea 7 | -------------------------------------------------------------------------------- /Gomfile: -------------------------------------------------------------------------------- 1 | gom "github.com/miekg/dns" 2 | gom "github.com/golang/glog" 3 | gom "github.com/bradfitz/gomemcache/memcache" 4 | gom "github.com/golang/groupcache/lru" 5 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package dkim 2 | 3 | var CustomHandlers struct { 4 | CacheGetHandler func(string) ([]byte, error) 5 | CacheSetHandler func(string, []byte) 6 | DnsFetchHandler func(string) ([]byte, error) 7 | } 8 | -------------------------------------------------------------------------------- /eml_test.go: -------------------------------------------------------------------------------- 1 | package dkim 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func Test_Ok(t *testing.T) { 10 | var fp *os.File 11 | 12 | fp, _ = os.Open("test_data/valid_1.eml") 13 | defer fp.Close() 14 | if ParseEml(bufio.NewReader(fp)).Verify() != true { 15 | t.Fail() 16 | } 17 | } 18 | 19 | func Test_MayBeNotOk(t *testing.T) { 20 | var fp *os.File 21 | 22 | fp, _ = os.Open("test_data/invalid_1.eml") 23 | defer fp.Close() 24 | if ParseEml(bufio.NewReader(fp)).Verify() == true { 25 | t.Fail() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /parse_eml.go: -------------------------------------------------------------------------------- 1 | package dkim 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "net/mail" 7 | ) 8 | 9 | func ParseEml(r *bufio.Reader) (*DKIM, error) { 10 | var Dkim *DKIM 11 | var msg *mail.Message 12 | var raw_headers mail.Header 13 | var err error 14 | var header string 15 | 16 | raw_headers, r = getRawHeaders(r) 17 | 18 | if msg, err = mail.ReadMessage(r); err != nil { 19 | return nil, err 20 | } 21 | 22 | if header, err = findDkimHeader(raw_headers); err != nil { 23 | return nil, errors.New("DKIM-Signature not found") 24 | } 25 | 26 | if Dkim, err = NewDKIM(header, msg); err != nil { 27 | return nil, err 28 | } 29 | Dkim.RawMailHeader = raw_headers 30 | return Dkim, nil 31 | } 32 | -------------------------------------------------------------------------------- /lru_handlers.go: -------------------------------------------------------------------------------- 1 | package dkim 2 | 3 | import ( 4 | "errors" 5 | lru "github.com/golang/groupcache/lru" 6 | ) 7 | 8 | var localCache *lru.Cache = lru.New(1000) 9 | 10 | func LocalCacheGetHandler(key string) ([]byte, error) { 11 | var value interface{} 12 | var ok bool 13 | 14 | if value, ok = localCache.Get(key); !ok { 15 | return nil, errors.New("Not found") 16 | } 17 | return value.([]byte), nil 18 | } 19 | func LocalCacheSetHandler(key string, value []byte) { 20 | if len(value) == 0 { 21 | return 22 | } 23 | localCache.Add(key, value) 24 | } 25 | 26 | func init() { 27 | if CustomHandlers.CacheGetHandler == nil { 28 | CustomHandlers.CacheGetHandler = LocalCacheGetHandler 29 | CustomHandlers.CacheSetHandler = LocalCacheSetHandler 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /memcache_handlers.go: -------------------------------------------------------------------------------- 1 | // +build memcache 2 | 3 | package dkim 4 | 5 | import ( 6 | "fmt" 7 | "github.com/bradfitz/gomemcache/memcache" 8 | ) 9 | 10 | func MemCacheGetHandler(key string) ([]byte, error) { 11 | var err error 12 | var mc *memcache.Client 13 | var item *memcache.Item 14 | mc = memcache.New("127.0.0.1:11211") 15 | 16 | if item, err = mc.Get(key); err == nil { 17 | return item.Value, nil 18 | } 19 | return nil, err 20 | } 21 | func MemCacheSetHandler(key string, value []byte) { 22 | if len(value) == 0 { 23 | return 24 | } 25 | mc := memcache.New("127.0.0.1:11211") 26 | mc.Set(&memcache.Item{Key: key, Value: value}) 27 | } 28 | 29 | func init() { 30 | CustomHandlers.CacheGetHandler = MemCacheGetHandler 31 | CustomHandlers.CacheSetHandler = MemCacheSetHandler 32 | } 33 | -------------------------------------------------------------------------------- /dns_handlers.go: -------------------------------------------------------------------------------- 1 | package dkim 2 | 3 | import ( 4 | "errors" 5 | "github.com/miekg/dns" 6 | "strings" 7 | ) 8 | 9 | func dnsFetchHandler(domain string) ([]byte, error) { 10 | var response *dns.Msg 11 | var txt *dns.TXT 12 | var ok bool 13 | var client *dns.Client = new(dns.Client) 14 | var msg *dns.Msg = new(dns.Msg) 15 | var err error 16 | 17 | msg.SetQuestion(domain, dns.TypeTXT) 18 | if response, _, err = client.Exchange(msg, dnsHost); err != nil { 19 | return nil, err 20 | } 21 | for _, rr := range response.Answer { 22 | if txt, ok = rr.(*dns.TXT); ok && len(txt.Txt) > 0 { 23 | return []byte(strings.Join(txt.Txt, "")), nil 24 | } 25 | } 26 | return nil, errors.New("not found") 27 | } 28 | 29 | var dnsHost string = "8.8.8.8:53" 30 | 31 | func SetDNS(ns string) { 32 | dnsHost = ns 33 | } 34 | 35 | func init() { 36 | CustomHandlers.DnsFetchHandler = dnsFetchHandler 37 | } 38 | -------------------------------------------------------------------------------- /struct.go: -------------------------------------------------------------------------------- 1 | package dkim 2 | 3 | import ( 4 | "hash" 5 | "net/mail" 6 | ) 7 | 8 | type DKIM struct { 9 | Header *DKIMHeader 10 | HeaderName string 11 | HeaderNameForHash string 12 | RawMailHeader mail.Header 13 | Hasher *hash.Hash 14 | Mail *mail.Message 15 | IsBodyRelaxed bool 16 | IsHeaderRelaxed bool 17 | Status struct { 18 | HasPublicKey bool 19 | ValidBody bool 20 | Valid bool 21 | } 22 | PublicKey *DKIMPublicKey 23 | headerHash []byte 24 | bodyHash []byte 25 | } 26 | 27 | type DKIMHeader struct { 28 | Version string `dkim:"v", json:"version"` 29 | Algorithm string `dkim:"a", json:"algorithm"` 30 | Canonization string `dkim:"c", json:"canonization"` 31 | Domain string `dkim:"d", json:"domain"` 32 | Selector string `dkim:"s", json:"selector"` 33 | Headers []string `dkim:"h", json:"headers"` 34 | Unixtime int `dkim:"t", json:"unixtime"` 35 | BodyHash []byte `dkim:"bh", json:"body_hash"` 36 | Signature []byte `dkim:"b", json:"signature"` 37 | Identifier string `dkim:"i", json:"identifier"` 38 | Length int `dkim:"l", json:"length"` 39 | QueryType string `dkim:"q", json:"query_type"` 40 | Expiration int `dkim:"x", json:"expiration"` 41 | CopiedHeaders map[string]string `dkim:"z", json:"copied_headers"` 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yet Another DKIM Verifier 2 | 3 | ## Why not? 4 | I found some DKIM solution for go, but I don’t like untested C binding and I have some time (thank you mr.Putin) 5 | I wrote this code for my pet project. 6 | 7 | ## What can we do? 8 | 9 | We can verify DKIM headers very fast. 10 | It is about 44mb per second on my laptop. 11 | We support custom resolving and custom cache logic. 12 | 13 | ## TODO 14 | - Support Length tag 15 | - Support Time 16 | - Support ExpireTime 17 | - Support Copied header fields (z=) 18 | - Sign 19 | 20 | ## How to use it? 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "bufio" 27 | "flag" 28 | "fmt" 29 | "os" 30 | 31 | "github.com/kalloc/dkim" 32 | ) 33 | 34 | // ./test path/to/emls/*.eml 35 | 36 | func main() { 37 | var ( 38 | filename string 39 | fp *os.File 40 | err error 41 | dk *dkim.DKIM 42 | ) 43 | 44 | flag.Parse() 45 | 46 | for _, filename = range flag.Args() { 47 | fmt.Printf("Check: %s — ", filename) 48 | if fp, err = os.Open(filename); err != nil { 49 | fmt.Printf("ERR-WRONG_FILE") 50 | } else if dk, _ = dkim.ParseEml(bufio.NewReader(fp)); dk == nil { 51 | fmt.Printf("ERR-DKIM_NOT_FOUND") 52 | } else if _, err = dk.GetPublicKey(); err != nil { 53 | fmt.Printf("ERR-DKIM_PK_NOT_FOUND") 54 | } else if dk.Verify() == false { 55 | fmt.Printf("ERR-DKIM_NOT_VERIFIED (Body is %v, Sig is %v)", dk.Status.ValidBody, dk.Status.Valid) 56 | } else { 57 | fmt.Printf("OK") 58 | } 59 | fmt.Printf("\n") 60 | } 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /verify.go: -------------------------------------------------------------------------------- 1 | package dkim 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "errors" 9 | "github.com/golang/glog" 10 | "strings" 11 | ) 12 | 13 | type DKIMPublicKey struct { 14 | Key string `dkim_pk:"k", json:"key"` 15 | Version string `dkim_pk:"v", json:"version"` 16 | Tag string `dkim_pk:"t", json:"version"` 17 | PublicKey []byte `dkim_pk:"p", json:"public_key"` 18 | } 19 | 20 | func newDKIMPublicKey(txt string) (*DKIMPublicKey, error) { 21 | var key, value, item string 22 | var kvs []string 23 | var err error 24 | var dkim_pk DKIMPublicKey 25 | for _, item = range strings.Split(strings.Replace(txt, " ", "", -1), ";") { 26 | kvs = strings.SplitN(item, "=", 2) 27 | if len(kvs) != 2 { 28 | continue 29 | } 30 | key = kvs[0] 31 | value = kvs[1] 32 | switch key { 33 | case "v": 34 | if value != "DKIM1" { 35 | return nil, errors.New("invalid version") 36 | } 37 | dkim_pk.Version = value 38 | break 39 | case "k": 40 | dkim_pk.Key = value 41 | break 42 | case "t": 43 | dkim_pk.Tag = value 44 | break 45 | case "p": 46 | if dkim_pk.PublicKey, err = base64.StdEncoding.DecodeString(value); err != nil { 47 | return nil, errors.New("invalid public") 48 | } 49 | break 50 | // default: 51 | // return nil, errors.New(fmt.Sprintf("unknown key %s", key)) 52 | } 53 | } 54 | if dkim_pk.PublicKey == nil { 55 | return nil, errors.New("empty") 56 | } 57 | return &dkim_pk, nil 58 | } 59 | func (Dkim *DKIM) GetPublicKey() (*DKIMPublicKey, error) { 60 | var domain string = Dkim.Header.Selector + "._domainkey." + Dkim.Header.Domain + "." 61 | var err error 62 | var public_key []byte 63 | var dkim_pk *DKIMPublicKey 64 | 65 | if Dkim.PublicKey != nil { 66 | return Dkim.PublicKey, nil 67 | } 68 | public_key, err = CustomHandlers.CacheGetHandler(domain) 69 | 70 | if err != nil { 71 | glog.Infof("CACHE MISS: %s (%s)\n", domain, err) 72 | if public_key, err = CustomHandlers.DnsFetchHandler(domain); err == nil { 73 | CustomHandlers.CacheSetHandler(domain, public_key) 74 | } 75 | } else { 76 | glog.Infof("CACHE HIT: %s -> %s\n", domain, public_key) 77 | } 78 | if err == nil { 79 | if dkim_pk, err = newDKIMPublicKey(string(public_key)); err != nil { 80 | return nil, err 81 | } 82 | Dkim.Status.HasPublicKey = true 83 | return dkim_pk, nil 84 | } 85 | return nil, errors.New("no public key found") 86 | } 87 | 88 | func (Dkim *DKIM) Verify() bool { 89 | var err error 90 | var pk interface{} 91 | 92 | if Dkim.Header.Domain != "" { 93 | Dkim.Status.ValidBody = bytes.Equal(Dkim.GetBodyHash(), Dkim.Header.BodyHash) 94 | glog.Infof("Calculated BodyHash %#v\n", Dkim.bodyHash) 95 | glog.Infof("Message BodyHash %#v\n", Dkim.Header.BodyHash) 96 | if Dkim.PublicKey, err = Dkim.GetPublicKey(); err == nil { 97 | 98 | if pk, err = x509.ParsePKIXPublicKey(Dkim.PublicKey.PublicKey); err == nil { 99 | if err = rsa.VerifyPKCS1v15(pk.(*rsa.PublicKey), Dkim.GetHasher(), Dkim.GetHeaderHash(), Dkim.Header.Signature); err == nil { 100 | Dkim.Status.Valid = true 101 | } 102 | } 103 | } 104 | } 105 | glog.Infof("Body: %v, Valid: %v, PK: %v\n", Dkim.Status.ValidBody, Dkim.Status.Valid, Dkim.Status.HasPublicKey) 106 | return Dkim.Status.ValidBody && Dkim.Status.Valid && Dkim.Status.HasPublicKey 107 | } 108 | -------------------------------------------------------------------------------- /test_data/valid_1.eml: -------------------------------------------------------------------------------- 1 | Delivered-To: martianvirgins@gmail.com 2 | Received: by 10.194.108.42 with SMTP id hh10csp2692387wjb; 3 | Sun, 4 Jan 2015 11:55:48 -0800 (PST) 4 | X-Received: by 10.194.91.145 with SMTP id ce17mr89654424wjb.132.1420401347756; 5 | Sun, 04 Jan 2015 11:55:47 -0800 (PST) 6 | Return-Path: 7 | Received: from mail-we0-f174.google.com (mail-we0-f174.google.com. [74.125.82.174]) 8 | by mx.google.com with ESMTPS id de1si107744922wjc.85.2015.01.04.11.55.46 9 | for 10 | (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); 11 | Sun, 04 Jan 2015 11:55:46 -0800 (PST) 12 | Received-SPF: softfail (google.com: domain of transitioning me+caf_=martianvirgins=gmail.com@n1ck.name does not designate 74.125.82.174 as permitted sender) client-ip=74.125.82.174; 13 | Authentication-Results: mx.google.com; 14 | spf=softfail (google.com: domain of transitioning me+caf_=martianvirgins=gmail.com@n1ck.name does not designate 74.125.82.174 as permitted sender) smtp.mail=me+caf_=martianvirgins=gmail.com@n1ck.name 15 | Received: by mail-we0-f174.google.com with SMTP id k48so6807807wev.19 16 | for ; Sun, 04 Jan 2015 11:55:46 -0800 (PST) 17 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 18 | d=1e100.net; s=20130820; 19 | h=x-original-authentication-results:x-gm-message-state:delivered-to 20 | :to:subject:message-id:date:from; 21 | bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=; 22 | b=HDpXHsYutafypL6GX97xyVUT6bZsJvUUVTXTpz9CIUL/2KVELC5KNK+pYDY57zPZ8P 23 | 95Ms7jDJcKnnJNX1ADuL/htzfZwXpfyxOagm9ERn47sh1UppEkweIs2x1wNOQFriCzY1 24 | uoM8D4QEk54Vk4S4+MCb28uRuLOFC3IR9cPlxLOdpMf4vMGMRCljXMpVMgQugV9ozudi 25 | y9iDsiEq9VhA1Aduu7I5QZZjn7hqXRQXIm5AEOuXlGEji0kPEaxisFavF8KbyUfez/Uv 26 | 6iuaaQce+VG+4a5777sVo+s+TQ5praDMFr0MRiRVZvddSMI3ex3Wa/LjNrVQkyMucBwh 27 | lXcw== 28 | X-Original-Authentication-Results: mx.google.com; spf=none (google.com: y@y.local does not designate permitted sender hosts) smtp.mail=y@y.local 29 | X-Gm-Message-State: ALoCoQmw+aMzqLzQtzOuvSykOY1g4F3BnuLJteMqyN0WFGuhbOYRFHTVj5fNAjHxuTrXIwtP1Osz 30 | X-Received: by 10.194.86.165 with SMTP id q5mr173883783wjz.10.1420401345967; 31 | Sun, 04 Jan 2015 11:55:45 -0800 (PST) 32 | X-Forwarded-To: martianvirgins@gmail.com 33 | X-Forwarded-For: me@n1ck.name martianvirgins@gmail.com 34 | Delivered-To: me@n1ck.name 35 | Received: by 10.194.25.200 with SMTP id e8csp4473252wjg; 36 | Sun, 4 Jan 2015 11:55:45 -0800 (PST) 37 | X-Received: by 10.112.24.130 with SMTP id u2mr87394753lbf.57.1420401344473; 38 | Sun, 04 Jan 2015 11:55:44 -0800 (PST) 39 | Return-Path: 40 | Received: from y.local (h86-62-83-99.ln.rinet.ru. [86.62.83.99]) 41 | by mx.google.com with ESMTP id la5si59534721lac.64.2015.01.04.11.55.43 42 | for ; 43 | Sun, 04 Jan 2015 11:55:44 -0800 (PST) 44 | Received-SPF: none (google.com: y@y.local does not designate permitted sender hosts) client-ip=86.62.83.99; 45 | Received: by y.local (Postfix, from userid 501) 46 | id 2F0F12623F04; Sun, 4 Jan 2015 22:55:41 +0300 (MSK) 47 | To: me@n1ck.name 48 | Subject: test 49 | Message-Id: <20150104195542.2F0F12623F04@y.local> 50 | Date: Sun, 4 Jan 2015 22:55:41 +0300 (MSK) 51 | From: y@y.local (y) 52 | 53 | test 54 | -------------------------------------------------------------------------------- /test_data/invalid_1.eml: -------------------------------------------------------------------------------- 1 | Delivered-To: martianvirgins@gmail.com 2 | Received: by 10.194.108.42 with SMTP id hh10csp2692387wjb; 3 | Sun, 4 Jan 2015 11:55:48 -0800 (PST) 4 | X-Received: by 10.194.91.145 with SMTP id ce17mr89654424wjb.132.1420401347756; 5 | Sun, 04 Jan 2015 11:55:47 -0800 (PST) 6 | Return-Path: 7 | Received: from mail-we0-f174.google.com (mail-we0-f174.google.com. [74.125.82.174]) 8 | by mx.google.com with ESMTPS id de1si107744922wjc.85.2015.01.04.11.55.46 9 | for 10 | (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); 11 | Sun, 04 Jan 2015 11:55:46 -0800 (PST) 12 | Received-SPF: softfail (google.com: domain of transitioning me+caf_=martianvirgins=gmail.com@n1ck.name does not designate 74.125.82.174 as permitted sender) client-ip=74.125.82.174; 13 | Authentication-Results: mx.google.com; 14 | spf=softfail (google.com: domain of transitioning me+caf_=martianvirgins=gmail.com@n1ck.name does not designate 74.125.82.174 as permitted sender) smtp.mail=me+caf_=martianvirgins=gmail.com@n1ck.name 15 | Received: by mail-we0-f174.google.com with SMTP id k48so6807807wev.19 16 | for ; Sun, 04 Jan 2015 11:55:46 -0800 (PST) 17 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 18 | d=1e100.net; s=20130820; 19 | h=x-original-authentication-results:x-gm-message-state:delivered-to 20 | :to:subject:message-id:date:from; 21 | bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=; 22 | b=HDpXHsYutafypL6GX97xyVUT6bZsJvUUVTXTpz9CIUL/2KVELC5KNK+pYDY57zPZ8P 23 | 95Ms7jDJcKnnJNX1ADuL/htzfZwXpfyxOagm9ERn47sh1UppEkweIs2x1wNOQFriCzY1 24 | uoM8D4QEk54Vk4S4+MCb28uRuLOFC3IR9cPlxLOdpMf4vMGMRCljXMpVMgQugV9ozudi 25 | y9iDsiEq9VhA1Aduu7I5QZZjn7hqXRQXIm5AEOuXlGEji0kPEaxisFavF8KbyUfez/Uv 26 | 6iuaaQce+VG+4a5777sVo+s+TQ5praDMFr0MRiRVZvddSMI3ex3Wa/LjNrVQkyMucBwh 27 | lXcw== 28 | X-Original-Authentication-Results: mx.google.com; spf=none (google.com: y@y.local does not designate permitted sender hosts) smtp.mail=y@y.local 29 | X-Gm-Message-State: ALoCoQmw+aMzqLzQtzOuvSykOY1g4F3BnuLJteMqyN0WFGuhbOYRFHTVj5fNAjHxuTrXIwtP1Osz 30 | X-Received: by 10.194.86.165 with SMTP id q5mr173883783wjz.10.1420401345967; 31 | Sun, 04 Jan 2015 11:55:45 -0800 (PST) 32 | X-Forwarded-To: martianvirgins@gmail.com 33 | X-Forwarded-For: me@n1ck.name martianvirgins@gmail.com 34 | Delivered-To: me@n1ck.name 35 | Received: by 10.194.25.200 with SMTP id e8csp4473252wjg; 36 | Sun, 4 Jan 2015 11:55:45 -0800 (PST) 37 | X-Received: by 10.112.24.130 with SMTP id u2mr87394753lbf.57.1420401344473; 38 | Sun, 04 Jan 2015 11:55:44 -0800 (PST) 39 | Return-Path: 40 | Received: from y.local (h86-62-83-99.ln.rinet.ru. [86.62.83.99]) 41 | by mx.google.com with ESMTP id la5si59534721lac.64.2015.01.04.11.55.43 42 | for ; 43 | Sun, 04 Jan 2015 11:55:44 -0800 (PST) 44 | Received-SPF: none (google.com: y@y.local does not designate permitted sender hosts) client-ip=86.62.83.99; 45 | Received: by y.local (Postfix, from userid 501) 46 | id 2F0F12623F04; Sun, 4 Jan 2015 22:55:41 +0300 (MSK) 47 | To: me@n1ck.name 48 | Subject: test 49 | Message-Id: <20150104195542.2F0F12623F04@y.local> 50 | Date: Sun, 4 Jan 2015 22:55:41 +0300 (MSK) 51 | From: y@y.local (y) 52 | 53 | testxx 54 | -------------------------------------------------------------------------------- /dkim.go: -------------------------------------------------------------------------------- 1 | package dkim 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto" 7 | "encoding/base64" 8 | "errors" 9 | "fmt" 10 | "github.com/golang/glog" 11 | "hash" 12 | "io" 13 | "net/mail" 14 | "net/textproto" 15 | "reflect" 16 | "regexp" 17 | "strconv" 18 | "strings" 19 | ) 20 | 21 | func NewDKIM(header string, msg *mail.Message) (*DKIM, error) { 22 | var err error 23 | var Dkim *DKIM = &DKIM{ 24 | Mail: msg, 25 | HeaderName: header, 26 | Header: &DKIMHeader{Version: "1"}, 27 | } 28 | if err = Dkim.parseDKIM(); err != nil { 29 | return nil, err 30 | } 31 | return Dkim, nil 32 | } 33 | 34 | func (Dkim *DKIM) GetHasher() crypto.Hash { 35 | switch Dkim.Header.Algorithm { 36 | case "rsa-sha256": 37 | return crypto.SHA256 38 | case "rsa-sha1": 39 | return crypto.SHA1 40 | default: 41 | panic("Unknown algorithm") 42 | } 43 | } 44 | 45 | func (Dkim *DKIMHeader) String() string { 46 | var st reflect.Type 47 | var item string 48 | var items []string 49 | var map_items []string 50 | var ok bool 51 | var map_item string 52 | var field reflect.StructField 53 | var value reflect.Value 54 | var byte_items []byte 55 | 56 | st = reflect.TypeOf(*Dkim) 57 | for i := 0; i < st.NumField(); i++ { 58 | field = st.Field(i) 59 | value = reflect.ValueOf(Dkim).Elem().Field(i) 60 | item = "" 61 | 62 | switch field.Type.Kind() { 63 | case reflect.Int: 64 | if value.Int() > 0 { 65 | item = strconv.Itoa(int(value.Int())) 66 | } 67 | break 68 | case reflect.String: 69 | item = value.String() 70 | break 71 | case reflect.Slice: 72 | if map_items, ok = value.Interface().([]string); ok { 73 | item = strings.Join(map_items, ":") 74 | } else if byte_items, ok = value.Interface().([]byte); ok { 75 | item = base64.StdEncoding.EncodeToString(byte_items) 76 | } else { 77 | panic("unknown type") 78 | } 79 | case reflect.Map: 80 | map_items = make([]string, 0) 81 | for _, key := range value.MapKeys() { 82 | map_item = key.String() + ":" + "reflect.ValueOf(field).MapIndex(key)" 83 | map_items = append(map_items, map_item) 84 | 85 | } 86 | item = strings.Join(map_items, "|") 87 | break 88 | 89 | default: 90 | panic(fmt.Sprintf("unknown kind %s", field.Type.Kind())) 91 | } 92 | if item != "" { 93 | items = append(items, field.Tag.Get("dkim")+"="+item) 94 | } 95 | 96 | } 97 | return strings.Join(items, "; ") 98 | } 99 | 100 | func (Dkim *DKIM) parseDKIM() (err error) { 101 | var prev_key, key, value, item string 102 | var kvs []string 103 | var header string 104 | 105 | header = Dkim.Mail.Header.Get(Dkim.HeaderName) 106 | for _, item = range strings.Split(strings.Replace(header, " ", "", -1), ";") { 107 | kvs = strings.SplitN(item, "=", 2) 108 | if len(kvs) != 2 { 109 | continue 110 | } 111 | key = kvs[0] 112 | value = kvs[1] 113 | switch key { 114 | case "v": 115 | if value != "1" { 116 | return errors.New("invalid version") 117 | } 118 | Dkim.Header.Version = value 119 | break 120 | case "a": 121 | Dkim.Header.Algorithm = value 122 | if value != "rsa-sha1" && value != "rsa-sha256" { 123 | panic(fmt.Sprintf("unknown algorithm %s", value)) 124 | } 125 | break 126 | case "d": 127 | Dkim.Header.Domain = value 128 | break 129 | case "c": 130 | Dkim.Header.Canonization = value 131 | Dkim.IsBodyRelaxed = strings.HasSuffix(value, "/relaxed") 132 | Dkim.IsHeaderRelaxed = strings.HasPrefix(value, "relaxed") 133 | break 134 | case "s": 135 | Dkim.Header.Selector = value 136 | break 137 | case "h": 138 | for _, key := range strings.Split(value, ":") { 139 | if prev_key != key { 140 | Dkim.Header.Headers = append(Dkim.Header.Headers, key) 141 | prev_key = key 142 | } 143 | } 144 | break 145 | case "t": 146 | if Dkim.Header.Unixtime, err = strconv.Atoi(value); err != nil { 147 | return errors.New("invalid time") 148 | } 149 | break 150 | case "bh": 151 | if Dkim.Header.BodyHash, err = base64.StdEncoding.DecodeString(value); err != nil { 152 | return errors.New("invalid body hash") 153 | } 154 | case "b": 155 | if Dkim.Header.Signature, err = base64.StdEncoding.DecodeString(value); err != nil { 156 | return errors.New("invalid signature") 157 | } 158 | break 159 | case "i": 160 | Dkim.Header.Identifier = value 161 | break 162 | case "l": 163 | if Dkim.Header.Length, err = strconv.Atoi(value); err != nil { 164 | return errors.New("invalid length") 165 | } 166 | break 167 | case "q": 168 | if value != "dns/txt" && value != "dns" { 169 | return errors.New("invalid query type") 170 | } 171 | Dkim.Header.QueryType = "dns/txt" 172 | break 173 | case "x": 174 | if Dkim.Header.Expiration, err = strconv.Atoi(value); err != nil { 175 | return errors.New("invalid expiration time") 176 | } 177 | break 178 | case "z": 179 | Dkim.Header.CopiedHeaders = make(map[string]string, 0) 180 | for _, header_item := range strings.Split(value, "|") { 181 | header_kv := strings.SplitN(header_item, ":", 2) 182 | Dkim.Header.CopiedHeaders[header_kv[0]] = header_kv[1] 183 | } 184 | break 185 | default: 186 | return errors.New(fmt.Sprintf("unknown key %s", key)) 187 | } 188 | } 189 | return 190 | } 191 | 192 | func (Dkim *DKIM) dkimSignatureForHash() []byte { 193 | var header mail.Header 194 | var rx *regexp.Regexp = regexp.MustCompile(`b=[^;]+`) 195 | 196 | if Dkim.IsHeaderRelaxed { 197 | header = Dkim.Mail.Header 198 | } else { 199 | header = Dkim.RawMailHeader 200 | } 201 | 202 | return rx.ReplaceAll([]byte(header.Get(Dkim.HeaderName)), []byte("b=")) 203 | } 204 | 205 | func (Dkim *DKIM) canonizeHeader(body []byte) []byte { 206 | if Dkim.IsHeaderRelaxed { 207 | if len(body) == 0 { 208 | return nil 209 | } 210 | rx := regexp.MustCompile(`[ \t]+`) 211 | body = rx.ReplaceAll(body, []byte(" ")) 212 | rx2 := regexp.MustCompile(` \r\n`) 213 | body = rx2.ReplaceAll(body, []byte("\r\n")) 214 | } else { 215 | if len(body) == 0 { 216 | return []byte("") 217 | } 218 | } 219 | return body 220 | 221 | } 222 | 223 | func findDkimHeader(mail_header mail.Header) (string, error) { 224 | var headers []string = []string{"DKIM-Signature", "X-Google-DKIM-Signature"} 225 | var header string 226 | 227 | for _, header = range headers { 228 | if mail_header.Get(header) != "" { 229 | return header, nil 230 | } 231 | } 232 | return "", errors.New("not found") 233 | } 234 | 235 | func getRawHeaders(r *bufio.Reader) (mail.Header, *bufio.Reader) { 236 | var headers mail.Header = make(map[string][]string, 0) 237 | 238 | var key, value, line string 239 | var kv []string 240 | var err error 241 | var readed *bytes.Buffer = bytes.NewBuffer([]byte("")) 242 | 243 | for { 244 | if line, err = r.ReadString('\n'); err != nil { 245 | break 246 | } 247 | readed.WriteString(line) 248 | 249 | if len(line) <= 2 { 250 | break 251 | } 252 | 253 | if line[0] != ' ' && line[0] != '\t' { 254 | kv = strings.SplitN(line, ":", 2) 255 | // skip invalid header 256 | if len(kv) == 2 { 257 | key = textproto.CanonicalMIMEHeaderKey(kv[0]) 258 | value = kv[1] 259 | if headers[key] == nil { 260 | headers[key] = make([]string, 0) 261 | } 262 | headers[key] = append(headers[key], value) 263 | } 264 | } else { 265 | headers[key][len(headers[key])-1] += line 266 | } 267 | 268 | } 269 | return headers, bufio.NewReader(io.MultiReader(bufio.NewReader(readed), r)) 270 | } 271 | 272 | func (Dkim *DKIM) GetHeaderHash() []byte { 273 | var hasher hash.Hash = Dkim.GetHasher().New() 274 | var header_key string 275 | var header_value string 276 | var header_values []string 277 | var dkim_sig_key string 278 | var header mail.Header 279 | 280 | if Dkim.headerHash != nil { 281 | return Dkim.headerHash 282 | } 283 | 284 | if Dkim.IsHeaderRelaxed { 285 | 286 | dkim_sig_key = "dkim-signature" 287 | header = Dkim.Mail.Header 288 | } else { 289 | dkim_sig_key = "DKIM-Signature" 290 | header = Dkim.RawMailHeader 291 | } 292 | 293 | for _, key := range Dkim.Header.Headers { 294 | if Dkim.IsHeaderRelaxed { 295 | header_key = strings.ToLower(key) 296 | } else { 297 | header_key = key 298 | } 299 | header_values = header[textproto.CanonicalMIMEHeaderKey(key)] 300 | // Skip header 301 | if len(header_values) == 0 { 302 | continue 303 | } 304 | header_value = header_values[len(header_values)-1] 305 | hasher.Write([]byte(header_key + ":")) 306 | hasher.Write(Dkim.canonizeHeader([]byte(header_value))) 307 | if Dkim.IsHeaderRelaxed { 308 | hasher.Write([]byte("\r\n")) 309 | } 310 | glog.Infof("%s:%s\r\n", header_key, Dkim.canonizeHeader([]byte(header_value))) 311 | } 312 | hasher.Write([]byte(dkim_sig_key + ":")) 313 | hasher.Write(Dkim.canonizeHeader(Dkim.dkimSignatureForHash())) 314 | glog.Infof("%s:%s\n", dkim_sig_key, Dkim.canonizeHeader(Dkim.dkimSignatureForHash())) 315 | Dkim.headerHash = hasher.Sum(nil) 316 | return Dkim.headerHash 317 | } 318 | 319 | func (Dkim *DKIM) readBodyTo(w io.Writer) { 320 | var line, prev_line, tmp []byte 321 | var r *bufio.Reader = bufio.NewReader(Dkim.Mail.Body) 322 | var err error 323 | 324 | for { 325 | line, err = r.ReadBytes('\n') 326 | if err != nil { 327 | break 328 | } 329 | if len(prev_line) > 0 { 330 | if len(tmp) > 0 { 331 | w.Write(tmp) 332 | tmp = nil 333 | } 334 | w.Write(prev_line) 335 | prev_line = nil 336 | } 337 | if Dkim.IsBodyRelaxed { 338 | if len(line) > 2 { 339 | line = bytes.TrimRight(line, "\t\r\n ") // remove WSP on the right side and trunk \r\n 340 | if len(line) > 0 { 341 | line = bytes.Replace(line, []byte("\t"), []byte(" "), -1) // replace all \t to \s 342 | line = regexp.MustCompile(`[ ]{2,}`).ReplaceAll(line, []byte(" ")) // replace double \s to one 343 | if line[0] == ' ' { // if the first symbol is \s, may be next too, replace all \s to one 344 | line = append([]byte(" "), bytes.TrimLeft(line, " ")...) 345 | } 346 | } 347 | line = append(line, '\r', '\n') 348 | } 349 | if len(line) > 2 { 350 | prev_line = line 351 | } else { 352 | tmp = append(tmp, '\r', '\n') 353 | } 354 | } else { 355 | prev_line = line 356 | } 357 | } 358 | if len(prev_line) > 0 { 359 | if !bytes.Equal(prev_line, []byte("\r\n")) { 360 | if len(tmp) > 0 { 361 | w.Write(tmp) 362 | } 363 | w.Write(prev_line) 364 | } 365 | } 366 | } 367 | 368 | func (Dkim *DKIM) GetBodyHash() []byte { 369 | if Dkim.bodyHash != nil { 370 | return Dkim.bodyHash 371 | } 372 | var hasher hash.Hash = Dkim.GetHasher().New() 373 | 374 | // for test 375 | // fp, _ := os.OpenFile("/tmp/go.txt", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) 376 | // defer fp.Close() 377 | // Dkim.readBodyTo(io.MultiWriter(fp, hasher)) 378 | 379 | Dkim.readBodyTo(hasher) 380 | 381 | Dkim.bodyHash = hasher.Sum(nil) 382 | return Dkim.bodyHash 383 | } 384 | --------------------------------------------------------------------------------