pax_global_header00006660000000000000000000000064147671232030014517gustar00rootroot0000000000000052 comment=da93f3891ba97ad9b075a1197e8cebf6bc0d6d7e golang-github-toorop-go-dkim-0.0~git20250226.9025cce/000077500000000000000000000000001476712320300215505ustar00rootroot00000000000000golang-github-toorop-go-dkim-0.0~git20250226.9025cce/.gitignore000066400000000000000000000004121476712320300235350ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof golang-github-toorop-go-dkim-0.0~git20250226.9025cce/LICENSE000066400000000000000000000021021476712320300225500ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Stéphane Depierrepont Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-toorop-go-dkim-0.0~git20250226.9025cce/README.md000066400000000000000000000024271476712320300230340ustar00rootroot00000000000000# go-dkim DKIM package for Golang [![GoDoc](https://godoc.org/github.com/toorop/go-dkim?status.svg)](https://godoc.org/github.com/toorop/go-dkim) ## Getting started ### Install ``` go get github.com/toorop/go-dkim ``` Warning: you need to use Go 1.4.2-master or 1.4.3 (when it will be available) see https://github.com/golang/go/issues/10482 fro more info. ### Sign email ```go import ( dkim "github.com/toorop/go-dkim" ) func main(){ // email is the email to sign (byte slice) // privateKey the private key (pem encoded, byte slice ) options := dkim.NewSigOptions() options.PrivateKey = privateKey options.Domain = "mydomain.tld" options.Selector = "myselector" options.SignatureExpireIn = 3600 options.BodyLength = 50 options.Headers = []string{"from", "date", "mime-version", "received", "received"} options.AddSignatureTimestamp = true options.Canonicalization = "relaxed/relaxed" err := dkim.Sign(&email, options) // handle err.. // And... that's it, 'email' is signed ! Amazing© !!! } ``` ### Verify ```go import ( dkim "github.com/toorop/go-dkim" ) func main(){ // email is the email to verify (byte slice) status, err := Verify(&email) // handle status, err (see godoc for status) } ``` ## Todo - [ ] handle z tag (copied header fields used for diagnostic use) golang-github-toorop-go-dkim-0.0~git20250226.9025cce/dkim.go000066400000000000000000000365111476712320300230310ustar00rootroot00000000000000// Package dkim provides tools for signing and verify a email according to RFC 6376 package dkim import ( "bytes" "container/list" "crypto" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "hash" "regexp" "strings" "time" ) const ( CRLF = "\r\n" TAB = " " FWS = CRLF + TAB MaxHeaderLineLength = 70 ) type verifyOutput int const ( SUCCESS verifyOutput = 1 + iota PERMFAIL TEMPFAIL NOTSIGNED TESTINGSUCCESS TESTINGPERMFAIL TESTINGTEMPFAIL ) // sigOptions represents signing options type SigOptions struct { // DKIM version (default 1) Version uint // Private key used for signing (required) PrivateKey []byte // 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 uint64 // CopiedHeaderFileds CopiedHeaderFields []string } // NewSigOptions 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: true, SignatureExpireIn: 0, } } // Sign signs an email func Sign(email *[]byte, options SigOptions) error { var privateKey *rsa.PrivateKey var err error // PrivateKey if len(options.PrivateKey) == 0 { return ErrSignPrivateKeyRequired } d, _ := pem.Decode(options.PrivateKey) if d == nil { return ErrCandNotParsePrivateKey } // try to parse it as PKCS1 otherwise try PKCS8 if key, err := x509.ParsePKCS1PrivateKey(d.Bytes); err != nil { if key, err := x509.ParsePKCS8PrivateKey(d.Bytes); err != nil { return ErrCandNotParsePrivateKey } else { privateKey = key.(*rsa.PrivateKey) } } else { privateKey = key } // Domain required if options.Domain == "" { return ErrSignDomainRequired } // Selector required if options.Selector == "" { return ErrSignSelectorRequired } // Canonicalization options.Canonicalization, err = validateCanonicalization(strings.ToLower(options.Canonicalization)) if err != nil { return err } // Algo options.Algo = strings.ToLower(options.Algo) if options.Algo != "rsa-sha1" && options.Algo != "rsa-sha256" { return ErrSignBadAlgo } // Header must contain "from" hasFrom := false for i, h := range options.Headers { h = strings.ToLower(h) options.Headers[i] = h if h == "from" { hasFrom = true } } if !hasFrom { return ErrSignHeaderShouldContainsFrom } // Normalize headers, body, err := canonicalize(email, options.Canonicalization, options.Headers) if err != nil { return err } signHash := strings.Split(options.Algo, "-") // hash body bodyHash, err := getBodyHash(&body, signHash[1], options.BodyLength) if err != nil { return err } // Get dkim header base dkimHeader := newDkimHeaderBySigOptions(options) dHeader := dkimHeader.getHeaderBaseForSigning(bodyHash) canonicalizations := strings.Split(options.Canonicalization, "/") dHeaderCanonicalized, err := canonicalizeHeader(dHeader, canonicalizations[0]) if err != nil { return err } headers = append(headers, []byte(dHeaderCanonicalized)...) headers = bytes.TrimRight(headers, " \r\n") // sign sig, err := getSignature(&headers, privateKey, signHash[1]) // add to DKIM-Header subh := "" l := len(subh) for _, c := range sig { subh += string(c) l++ if l >= MaxHeaderLineLength { dHeader += subh + FWS subh = "" l = 0 } } dHeader += subh + CRLF *email = append([]byte(dHeader), *email...) return nil } // Verify verifies an email an return // state: SUCCESS or PERMFAIL or TEMPFAIL, TESTINGSUCCESS, TESTINGPERMFAIL // TESTINGTEMPFAIL or NOTSIGNED // error: if an error occurs during verification func Verify(email *[]byte, opts ...DNSOpt) (verifyOutput, error) { // parse email dkimHeader, err := GetHeader(email) if err != nil { if err == ErrDkimHeaderNotFound { return NOTSIGNED, ErrDkimHeaderNotFound } return PERMFAIL, err } // we do not set query method because if it's others, validation failed earlier pubKey, verifyOutputOnError, err := NewPubKeyRespFromDNS(dkimHeader.Selector, dkimHeader.Domain, opts...) if err != nil { // fix https://github.com/toorop/go-dkim/issues/1 // return getVerifyOutput(verifyOutputOnError, err, pubKey.FlagTesting) return verifyOutputOnError, err } // Normalize headers, body, err := canonicalize(email, dkimHeader.MessageCanonicalization, dkimHeader.Headers) if err != nil { return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) } sigHash := strings.Split(dkimHeader.Algorithm, "-") // check if hash algo are compatible compatible := false for _, algo := range pubKey.HashAlgo { if sigHash[1] == algo { compatible = true break } } if !compatible { return getVerifyOutput(PERMFAIL, ErrVerifyInappropriateHashAlgo, pubKey.FlagTesting) } // expired ? if !dkimHeader.SignatureExpiration.IsZero() && dkimHeader.SignatureExpiration.Before(time.Now()) { return getVerifyOutput(PERMFAIL, ErrVerifySignatureHasExpired, pubKey.FlagTesting) } // println("|" + string(body) + "|") // get body hash bodyHash, err := getBodyHash(&body, sigHash[1], dkimHeader.BodyLength) if err != nil { return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) } // println(bodyHash) if bodyHash != dkimHeader.BodyHash { return getVerifyOutput(PERMFAIL, ErrVerifyBodyHash, pubKey.FlagTesting) } // compute sig dkimHeaderCano, err := canonicalizeHeader(dkimHeader.rawForSign, strings.Split(dkimHeader.MessageCanonicalization, "/")[0]) if err != nil { return getVerifyOutput(TEMPFAIL, err, pubKey.FlagTesting) } toSignStr := string(headers) + dkimHeaderCano toSign := bytes.TrimRight([]byte(toSignStr), " \r\n") err = verifySignature(toSign, dkimHeader.SignatureData, &pubKey.PubKey, sigHash[1]) if err != nil { return getVerifyOutput(PERMFAIL, err, pubKey.FlagTesting) } return SUCCESS, nil } // getVerifyOutput returns output of verify fct according to the testing flag func getVerifyOutput(status verifyOutput, err error, flagTesting bool) (verifyOutput, error) { if !flagTesting { return status, err } switch status { case SUCCESS: return TESTINGSUCCESS, err case PERMFAIL: return TESTINGPERMFAIL, err case TEMPFAIL: return TESTINGTEMPFAIL, err } // should never happen but compilator sream whithout return return status, err } // canonicalize returns canonicalized version of header and body func canonicalize(email *[]byte, cano string, h []string) (headers, body []byte, err error) { body = []byte{} rxReduceWS := regexp.MustCompile(`[ \t]+`) rawHeaders, rawBody, err := getHeadersBody(email) if err != nil { return nil, nil, err } canonicalizations := strings.Split(cano, "/") // canonicalyze header headersList, err := getHeadersList(&rawHeaders) // pour chaque header a conserver on traverse tous les headers dispo // If multi instance of a field we must keep it from the bottom to the top var match *list.Element headersToKeepList := list.New() for _, headerToKeep := range h { match = nil headerToKeepToLower := strings.ToLower(headerToKeep) for e := headersList.Front(); e != nil; e = e.Next() { // fmt.Printf("|%s|\n", e.Value.(string)) t := strings.Split(e.Value.(string), ":") if strings.ToLower(t[0]) == headerToKeepToLower { match = e } } if match != nil { headersToKeepList.PushBack(match.Value.(string) + "\r\n") headersList.Remove(match) } } // if canonicalizations[0] == "simple" { for e := headersToKeepList.Front(); e != nil; e = e.Next() { cHeader, err := canonicalizeHeader(e.Value.(string), canonicalizations[0]) if err != nil { return headers, body, err } headers = append(headers, []byte(cHeader)...) } // canonicalyze body if canonicalizations[1] == "simple" { // simple // The "simple" body canonicalization algorithm ignores all empty lines // at the end of the message body. An empty line is a line of zero // length after removal of the line terminator. If there is no body or // no trailing CRLF on the message body, a CRLF is added. It makes no // other changes to the message body. In more formal terms, the // "simple" body canonicalization algorithm converts "*CRLF" at the end // of the body to a single "CRLF". // Note that a completely empty or missing body is canonicalized as a // single "CRLF"; that is, the canonicalized length will be 2 octets. body = bytes.TrimRight(rawBody, "\r\n") body = append(body, []byte{13, 10}...) } else { // relaxed // Ignore all whitespace at the end of lines. Implementations // MUST NOT remove the CRLF at the end of the line. // Reduce all sequences of WSP within a line to a single SP // character. // Ignore all empty lines at the end of the message body. "Empty // line" is defined in Section 3.4.3. If the body is non-empty but // does not end with a CRLF, a CRLF is added. (For email, this is // only possible when using extensions to SMTP or non-SMTP transport // mechanisms.) rawBody = rxReduceWS.ReplaceAll(rawBody, []byte(" ")) for _, line := range bytes.SplitAfter(rawBody, []byte{10}) { line = bytes.TrimRight(line, " \r\n") body = append(body, line...) body = append(body, []byte{13, 10}...) } body = bytes.TrimRight(body, "\r\n") body = append(body, []byte{13, 10}...) } return } // canonicalizeHeader returns canonicalized version of header func canonicalizeHeader(header string, algo string) (string, error) { // rxReduceWS := regexp.MustCompile(`[ \t]+`) if algo == "simple" { // The "simple" header canonicalization algorithm does not change header // fields in any way. Header fields MUST be presented to the signing or // verification algorithm exactly as they are in the message being // signed or verified. In particular, header field names MUST NOT be // case folded and whitespace MUST NOT be changed. return header, nil } else if algo == "relaxed" { // The "relaxed" header canonicalization algorithm MUST apply the // following steps in order: // Convert all header field names (not the header field values) to // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC". // Unfold all header field continuation lines as described in // [RFC5322]; in particular, lines with terminators embedded in // continued header field values (that is, CRLF sequences followed by // WSP) MUST be interpreted without the CRLF. Implementations MUST // NOT remove the CRLF at the end of the header field value. // Convert all sequences of one or more WSP characters to a single SP // character. WSP characters here include those before and after a // line folding boundary. // Delete all WSP characters at the end of each unfolded header field // value. // Delete any WSP characters remaining before and after the colon // separating the header field name from the header field value. The // colon separator MUST be retained. kv := strings.SplitN(header, ":", 2) if len(kv) != 2 { return header, ErrBadMailFormatHeaders } k := strings.ToLower(kv[0]) k = strings.TrimSpace(k) v := removeFWS(kv[1]) // v = rxReduceWS.ReplaceAllString(v, " ") // v = strings.TrimSpace(v) return k + ":" + v + CRLF, nil } return header, ErrSignBadCanonicalization } // getBodyHash return the hash (bas64encoded) of the body func getBodyHash(body *[]byte, algo string, bodyLength uint) (string, error) { var h hash.Hash if algo == "sha1" { h = sha1.New() } else { h = sha256.New() } toH := *body // if l tag (body length) if bodyLength != 0 { if uint(len(toH)) < bodyLength { return "", ErrBadDKimTagLBodyTooShort } toH = toH[0:bodyLength] } h.Write(toH) return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil } // getSignature return signature of toSign using key func getSignature(toSign *[]byte, key *rsa.PrivateKey, algo string) (string, error) { var h1 hash.Hash var h2 crypto.Hash switch algo { case "sha1": h1 = sha1.New() h2 = crypto.SHA1 break case "sha256": h1 = sha256.New() h2 = crypto.SHA256 break default: return "", ErrVerifyInappropriateHashAlgo } // sign h1.Write(*toSign) sig, err := rsa.SignPKCS1v15(rand.Reader, key, h2, h1.Sum(nil)) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(sig), nil } // verifySignature verify signature from pubkey func verifySignature(toSign []byte, sig64 string, key *rsa.PublicKey, algo string) error { var h1 hash.Hash var h2 crypto.Hash switch algo { case "sha1": h1 = sha1.New() h2 = crypto.SHA1 break case "sha256": h1 = sha256.New() h2 = crypto.SHA256 break default: return ErrVerifyInappropriateHashAlgo } h1.Write(toSign) sig, err := base64.StdEncoding.DecodeString(sig64) if err != nil { return err } return rsa.VerifyPKCS1v15(key, h2, h1.Sum(nil), sig) } // removeFWS removes all FWS from string func removeFWS(in string) string { rxReduceWS := regexp.MustCompile(`[ \t]+`) out := strings.Replace(in, "\n", "", -1) out = strings.Replace(out, "\r", "", -1) out = rxReduceWS.ReplaceAllString(out, " ") return strings.TrimSpace(out) } // validateCanonicalization validate canonicalization (c flag) func validateCanonicalization(cano string) (string, error) { p := strings.Split(cano, "/") if len(p) > 2 { return "", ErrSignBadCanonicalization } if len(p) == 1 { cano = cano + "/simple" } for _, c := range p { if c != "simple" && c != "relaxed" { return "", ErrSignBadCanonicalization } } return cano, nil } // getHeadersList returns headers as list func getHeadersList(rawHeader *[]byte) (*list.List, error) { headersList := list.New() currentHeader := []byte{} for _, line := range bytes.SplitAfter(*rawHeader, []byte{10}) { if line[0] == 32 || line[0] == 9 { if len(currentHeader) == 0 { return headersList, ErrBadMailFormatHeaders } currentHeader = append(currentHeader, line...) } else { // New header, save current if exists if len(currentHeader) != 0 { headersList.PushBack(string(bytes.TrimRight(currentHeader, "\r\n"))) currentHeader = []byte{} } currentHeader = append(currentHeader, line...) } } headersList.PushBack(string(currentHeader)) return headersList, nil } // getHeadersBody return headers and body func getHeadersBody(email *[]byte) ([]byte, []byte, error) { substitutedEmail := *email // only replace \n with \r\n when \r\n\r\n not exists if bytes.Index(*email, []byte{13, 10, 13, 10}) < 0 { // \n -> \r\n substitutedEmail = bytes.Replace(*email, []byte{10}, []byte{13, 10}, -1) } parts := bytes.SplitN(substitutedEmail, []byte{13, 10, 13, 10}, 2) if len(parts) != 2 { return []byte{}, []byte{}, ErrBadMailFormat } // Empty body if len(parts[1]) == 0 { parts[1] = []byte{13, 10} } return parts[0], parts[1], nil } golang-github-toorop-go-dkim-0.0~git20250226.9025cce/dkimHeader.go000066400000000000000000000432061476712320300241410ustar00rootroot00000000000000package dkim import ( "bytes" "fmt" "net/mail" "net/textproto" "strconv" "strings" "time" ) type DKIMHeader struct { // Version This tag defines the version of DKIM // specification that applies to the signature record. // tag v Version string // The algorithm used to generate the signature.. // Verifiers MUST support "rsa-sha1" and "rsa-sha256"; // Signers SHOULD sign using "rsa-sha256". // tag a Algorithm string // The signature data (base64). // Whitespace is ignored in this value and MUST be // ignored when reassembling the original signature. // In particular, the signing process can safely insert // FWS in this value in arbitrary places to conform to line-length // limits. // tag b SignatureData string // The hash of the canonicalized body part of the message as // limited by the "l=" tag (base64; REQUIRED). // Whitespace is ignored in this value and MUST be ignored when reassembling the original // signature. In particular, the signing process can safely insert // FWS in this value in arbitrary places to conform to line-length // limits. // tag bh BodyHash 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. It // consists of two names separated by a "slash" (%d47) character, // corresponding to the header and body canonicalization algorithms, // respectively. These algorithms are described in Section 3.4. If // only one algorithm is named, that algorithm is used for the header // and "simple" is used for the body. For example, "c=relaxed" is // treated the same as "c=relaxed/simple". // tag c MessageCanonicalization string // The SDID claiming responsibility for an introduction of a message // into the mail stream (plain-text; REQUIRED). Hence, the SDID // value is used to form the query for the public key. The SDID MUST // correspond to a valid DNS name under which the DKIM key record is // published. The conventions and semantics used by a Signer to // create and use a specific SDID are outside the scope of this // specification, as is any use of those conventions and semantics. // When presented with a signature that does not meet these // requirements, Verifiers MUST consider the signature invalid. // Internationalized domain names MUST be encoded as A-labels, as // described in Section 2.3 of [RFC5890]. // tag d Domain string // Signed header fields (plain-text, but see description; REQUIRED). // A colon-separated list of header field names that identify the // header fields presented to the signing algorithm. The field MUST // contain the complete list of header fields in the order presented // to the signing algorithm. The field MAY contain names of header // fields that do not exist when signed; nonexistent header fields do // not contribute to the signature computation (that is, they are // treated as the null input, including the header field name, the // separating colon, the header field value, and any CRLF // terminator). The field MAY contain multiple instances of a header // field name, meaning multiple occurrences of the corresponding // header field are included in the header hash. The field MUST NOT // include the DKIM-Signature header field that is being created or // verified but may include others. Folding whitespace (FWS) MAY be // included on either side of the colon separator. Header field // names MUST be compared against actual header field names in a // case-insensitive manner. This list MUST NOT be empty. See // Section 5.4 for a discussion of choosing header fields to sign and // Section 5.4.2 for requirements when signing multiple instances of // a single field. // tag h Headers []string // The Agent or User Identifier (AUID) on behalf of which the SDID is // taking responsibility (dkim-quoted-printable; OPTIONAL, default is // an empty local-part followed by an "@" followed by the domain from // the "d=" tag). // The syntax is a standard email address where the local-part MAY be // omitted. The domain part of the address MUST be the same as, or a // subdomain of, the value of the "d=" tag. // Internationalized domain names MUST be encoded as A-labels, as // described in Section 2.3 of [RFC5890]. // tag i Auid string // Body length count (plain-text unsigned decimal integer; OPTIONAL, // default is entire body). This tag informs the Verifier of the // number of octets in the body of the email after canonicalization // included in the cryptographic hash, starting from 0 immediately // following the CRLF preceding the body. This value MUST NOT be // larger than the actual number of octets in the canonicalized // message body. See further discussion in Section 8.2. // tag l BodyLength uint // A colon-separated list of query methods used to retrieve the // public key (plain-text; OPTIONAL, default is "dns/txt"). Each // query method is of the form "type[/options]", where the syntax and // semantics of the options depend on the type and specified options. // If there are multiple query mechanisms listed, the choice of query // mechanism MUST NOT change the interpretation of the signature. // Implementations MUST use the recognized query mechanisms in the // order presented. Unrecognized query mechanisms MUST be ignored. // Currently, the only valid value is "dns/txt", which defines the // DNS TXT resource record (RR) lookup algorithm described elsewhere // in this document. The only option defined for the "dns" query // type is "txt", which MUST be included. Verifiers and Signers MUST // support "dns/txt". // tag q QueryMethods []string // The selector subdividing the namespace for the "d=" (domain) tag // (plain-text; REQUIRED). // Internationalized selector names MUST be encoded as A-labels, as // described in Section 2.3 of [RFC5890]. // tag s Selector string // Signature Timestamp (plain-text unsigned decimal integer; // RECOMMENDED, default is an unknown creation time). The time that // this signature was created. The format is the number of seconds // since 00:00:00 on January 1, 1970 in the UTC time zone. The value // is expressed as an unsigned integer in decimal ASCII. This value // is not constrained to fit into a 31- or 32-bit integer. // Implementations SHOULD be prepared to handle values up to at least // 10^12 (until approximately AD 200,000; this fits into 40 bits). // To avoid denial-of-service attacks, implementations MAY consider // any value longer than 12 digits to be infinite. Leap seconds are // not counted. Implementations MAY ignore signatures that have a // timestamp in the future. // tag t SignatureTimestamp time.Time // Signature Expiration (plain-text unsigned decimal integer; // RECOMMENDED, default is no expiration). The format is the same as // in the "t=" tag, represented as an absolute date, not as a time // delta from the signing timestamp. The value is expressed as an // unsigned integer in decimal ASCII, with the same constraints on // the value in the "t=" tag. Signatures MAY be considered invalid // if the verification time at the Verifier is past the expiration // date. The verification time should be the time that the message // was first received at the administrative domain of the Verifier if // that time is reliably available; otherwise, the current time // should be used. The value of the "x=" tag MUST be greater than // the value of the "t=" tag if both are present. //tag x SignatureExpiration time.Time // Copied header fields (dkim-quoted-printable, but see description; // OPTIONAL, default is null). A vertical-bar-separated list of // selected header fields present when the message was signed, // including both the field name and value. It is not required to // include all header fields present at the time of signing. This // field need not contain the same header fields listed in the "h=" // tag. The header field text itself must encode the vertical bar // ("|", %x7C) character (i.e., vertical bars in the "z=" text are // meta-characters, and any actual vertical bar characters in a // copied header field must be encoded). Note that all whitespace // must be encoded, including whitespace between the colon and the // header field value. After encoding, FWS MAY be added at arbitrary // locations in order to avoid excessively long lines; such // whitespace is NOT part of the value of the header field and MUST // be removed before decoding. // The header fields referenced by the "h=" tag refer to the fields // in the [RFC5322] header of the message, not to any copied fields // in the "z=" tag. Copied header field values are for diagnostic // use. // tag z CopiedHeaderFields []string // HeaderMailFromDomain store the raw email address of the header Mail From // used for verifying in case of multiple DKIM header (we will prioritise // header with d = mail from domain) //HeaderMailFromDomain string // RawForsign represents the raw part (without canonicalization) of the header // used for computint sig in verify process rawForSign string } // NewDkimHeaderBySigOptions return a new DkimHeader initioalized with sigOptions value func newDkimHeaderBySigOptions(options SigOptions) *DKIMHeader { h := new(DKIMHeader) h.Version = "1" h.Algorithm = options.Algo h.MessageCanonicalization = options.Canonicalization h.Domain = options.Domain h.Headers = options.Headers h.Auid = options.Auid h.BodyLength = options.BodyLength h.QueryMethods = options.QueryMethods h.Selector = options.Selector if options.AddSignatureTimestamp { h.SignatureTimestamp = time.Now() } if options.SignatureExpireIn > 0 { h.SignatureExpiration = time.Now().Add(time.Duration(options.SignatureExpireIn) * time.Second) } h.CopiedHeaderFields = options.CopiedHeaderFields return h } // GetHeader return a new DKIMHeader by parsing an email // Note: according to RFC 6376 an email can have multiple DKIM Header // in this case we return the last inserted or the last with d== mail from func GetHeader(email *[]byte) (*DKIMHeader, error) { m, err := mail.ReadMessage(bytes.NewReader(*email)) if err != nil { return nil, err } // DKIM header ? if len(m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")]) == 0 { return nil, ErrDkimHeaderNotFound } // Get mail from domain mailFromDomain := "" mailfrom, err := mail.ParseAddress(m.Header.Get(textproto.CanonicalMIMEHeaderKey("From"))) if err != nil { if err.Error() != "mail: no address" { return nil, err } } else { t := strings.SplitAfter(mailfrom.Address, "@") if len(t) > 1 { mailFromDomain = strings.ToLower(t[1]) } } // get raw dkim header // we can't use m.header because header key will be converted with textproto.CanonicalMIMEHeaderKey // ie if key in header is not DKIM-Signature but Dkim-Signature or DKIM-signature ot... other // combination of case, verify will fail. rawHeaders, _, err := getHeadersBody(email) if err != nil { return nil, ErrBadMailFormat } rawHeadersList, err := getHeadersList(&rawHeaders) if err != nil { return nil, err } dkHeaders := []string{} for h := rawHeadersList.Front(); h != nil; h = h.Next() { if strings.HasPrefix(strings.ToLower(h.Value.(string)), "dkim-signature") { dkHeaders = append(dkHeaders, h.Value.(string)) } } var keep *DKIMHeader var keepErr error //for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] { for _, h := range dkHeaders { parsed, err := parseDkHeader(h) // if malformed dkim header try next if err != nil { keepErr = err continue } // Keep first dkim headers if keep == nil { keep = parsed } // if d flag == domain keep this header and return if mailFromDomain == parsed.Domain { return parsed, nil } } if keep == nil { return nil, keepErr } return keep, nil } // parseDkHeader parse raw dkim header func parseDkHeader(header string) (dkh *DKIMHeader, err error) { dkh = new(DKIMHeader) keyVal := strings.SplitN(header, ":", 2) t := strings.LastIndex(header, "b=") if t == -1 { return nil, ErrDkimHeaderBTagNotFound } dkh.rawForSign = header[0 : t+2] p := strings.IndexByte(header[t:], ';') if p != -1 { dkh.rawForSign = dkh.rawForSign + header[t+p:] } // Mandatory mandatoryFlags := make(map[string]bool, 7) //(b'v', b'a', b'b', b'bh', b'd', b'h', b's') mandatoryFlags["v"] = false mandatoryFlags["a"] = false mandatoryFlags["b"] = false mandatoryFlags["bh"] = false mandatoryFlags["d"] = false mandatoryFlags["h"] = false mandatoryFlags["s"] = false // default values dkh.MessageCanonicalization = "simple/simple" dkh.QueryMethods = []string{"dns/txt"} // unfold && clean val := removeFWS(keyVal[1]) val = strings.Replace(val, " ", "", -1) fs := strings.Split(val, ";") for _, f := range fs { if f == "" { continue } flagData := strings.SplitN(f, "=", 2) // https://github.com/toorop/go-dkim/issues/2 // if flag is not in the form key=value (eg doesn't have "=") if len(flagData) != 2 { return nil, ErrDkimHeaderBadFormat } flag := strings.ToLower(strings.TrimSpace(flagData[0])) data := strings.TrimSpace(flagData[1]) switch flag { case "v": if data != "1" { return nil, ErrDkimVersionNotsupported } dkh.Version = data mandatoryFlags["v"] = true case "a": dkh.Algorithm = strings.ToLower(data) if dkh.Algorithm != "rsa-sha1" && dkh.Algorithm != "rsa-sha256" { return nil, ErrSignBadAlgo } mandatoryFlags["a"] = true case "b": //dkh.SignatureData = removeFWS(data) // remove all space dkh.SignatureData = strings.Replace(removeFWS(data), " ", "", -1) if len(dkh.SignatureData) != 0 { mandatoryFlags["b"] = true } case "bh": dkh.BodyHash = removeFWS(data) if len(dkh.BodyHash) != 0 { mandatoryFlags["bh"] = true } case "d": dkh.Domain = strings.ToLower(data) if len(dkh.Domain) != 0 { mandatoryFlags["d"] = true } case "h": data = strings.ToLower(data) dkh.Headers = strings.Split(data, ":") if len(dkh.Headers) != 0 { mandatoryFlags["h"] = true } fromFound := false for _, h := range dkh.Headers { if h == "from" { fromFound = true } } if !fromFound { return nil, ErrDkimHeaderNoFromInHTag } case "s": dkh.Selector = strings.ToLower(data) if len(dkh.Selector) != 0 { mandatoryFlags["s"] = true } case "c": dkh.MessageCanonicalization, err = validateCanonicalization(strings.ToLower(data)) if err != nil { return nil, err } case "i": if data != "" { if !strings.HasSuffix(data, dkh.Domain) { return nil, ErrDkimHeaderDomainMismatch } dkh.Auid = data } case "l": ui, err := strconv.ParseUint(data, 10, 32) if err != nil { return nil, err } dkh.BodyLength = uint(ui) case "q": dkh.QueryMethods = strings.Split(data, ":") if len(dkh.QueryMethods) == 0 || strings.ToLower(dkh.QueryMethods[0]) != "dns/txt" { return nil, errQueryMethodNotsupported } case "t": ts, err := strconv.ParseInt(data, 10, 64) if err != nil { return nil, err } dkh.SignatureTimestamp = time.Unix(ts, 0) case "x": ts, err := strconv.ParseInt(data, 10, 64) if err != nil { return nil, err } dkh.SignatureExpiration = time.Unix(ts, 0) case "z": dkh.CopiedHeaderFields = strings.Split(data, "|") } } // All mandatory flags are in ? for _, p := range mandatoryFlags { if !p { return nil, ErrDkimHeaderMissingRequiredTag } } // default for i/Auid if dkh.Auid == "" { dkh.Auid = "@" + dkh.Domain } // defaut for query method if len(dkh.QueryMethods) == 0 { dkh.QueryMethods = []string{"dns/text"} } return dkh, nil } // GetHeaderBase return base header for signers // Todo: some refactoring needed... func (d *DKIMHeader) getHeaderBaseForSigning(bodyHash string) string { h := "DKIM-Signature: v=" + d.Version + "; a=" + d.Algorithm + "; q=" + strings.Join(d.QueryMethods, ":") + "; c=" + d.MessageCanonicalization + ";" + CRLF + TAB subh := "s=" + d.Selector + ";" if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength { h += subh + FWS subh = "" } subh += " d=" + d.Domain + ";" // Auid if len(d.Auid) != 0 { if len(subh)+len(d.Auid)+4 > MaxHeaderLineLength { h += subh + FWS subh = "" } subh += " i=" + d.Auid + ";" } /*h := "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tmail.io; i=@tmail.io;" + FWS subh := "q=dns/txt; s=test;"*/ // signature timestamp if !d.SignatureTimestamp.IsZero() { ts := d.SignatureTimestamp.Unix() if len(subh)+14 > MaxHeaderLineLength { h += subh + FWS subh = "" } subh += " t=" + fmt.Sprintf("%d", ts) + ";" } if len(subh)+len(d.Domain)+4 > MaxHeaderLineLength { h += subh + FWS subh = "" } // Expiration if !d.SignatureExpiration.IsZero() { ts := d.SignatureExpiration.Unix() if len(subh)+14 > MaxHeaderLineLength { h += subh + FWS subh = "" } subh += " x=" + fmt.Sprintf("%d", ts) + ";" } // body length if d.BodyLength != 0 { bodyLengthStr := fmt.Sprintf("%d", d.BodyLength) if len(subh)+len(bodyLengthStr)+4 > MaxHeaderLineLength { h += subh + FWS subh = "" } subh += " l=" + bodyLengthStr + ";" } // Headers if len(subh)+len(d.Headers)+4 > MaxHeaderLineLength { h += subh + FWS subh = "" } subh += " h=" for _, header := range d.Headers { if len(subh)+len(header)+1 > MaxHeaderLineLength { h += subh + FWS subh = "" } subh += header + ":" } subh = subh[:len(subh)-1] + ";" // BodyHash if len(subh)+5+len(bodyHash) > MaxHeaderLineLength { h += subh + FWS subh = "" } else { subh += " " } subh += "bh=" l := len(subh) for _, c := range bodyHash { subh += string(c) l++ if l >= MaxHeaderLineLength { h += subh + FWS subh = "" l = 0 } } h += subh + ";" + FWS + "b=" return h } golang-github-toorop-go-dkim-0.0~git20250226.9025cce/dkimHeader_test.go000066400000000000000000000026421476712320300251770ustar00rootroot00000000000000package dkim import ( "testing" "github.com/go-test/deep" ) func Test_GetHeader(t *testing.T) { tests := []struct { name string input string want *DKIMHeader wantErr bool }{ { name: "Signed relaxed with length", input: signedRelaxedRelaxedLength, want: &DKIMHeader{ Version: "1", Algorithm: "rsa-sha256", QueryMethods: []string{"dns/txt"}, MessageCanonicalization: "relaxed/relaxed", Selector: "test", Domain: "tmail.io", Auid: "@tmail.io", BodyLength: 5, Headers: []string{"from", "date", "mime-version", "received", "received"}, BodyHash: "GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=", SignatureData: "byhiFWd0lAM1sqD1tl8S1DZtKNqgiEZp8jrGds6RRydnZkdX9rCPeL0Q5MYWBQ/JmQrml5" + "pIghLwl/EshDBmNy65O6qO8pSSGgZmM3T7SRLMloex8bnrBJ4KSYcHV46639gVEWcBOKW0" + "h1djZu2jaTuxGeJzlFVtw3Arf2B93cc=", }, }, { name: "No signature", input: bodySimple, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { email := []byte(tt.input) got, err := GetHeader(&email) if (err != nil) != tt.wantErr { t.Errorf("GetHeader() error = %v, wantErr %v", err, tt.wantErr) return } if diff := deep.Equal(tt.want, got); diff != nil { t.Error(diff) } }) } } golang-github-toorop-go-dkim-0.0~git20250226.9025cce/dkim_test.go000066400000000000000000000455641476712320300241000ustar00rootroot00000000000000package dkim import ( //"fmt" "crypto/rsa" "crypto/x509" "encoding/pem" "net" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( privKey = `-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDNUXO+Qsl1tw+GjrqFajz0ERSEUs1FHSL/+udZRWn1Atw8gz0+ tcGqhWChBDeU9gY5sKLEAZnX3FjC/T/IbqeiSM68kS5vLkzRI84eiJrm3+IieUqI IicsO+WYxQs+JgVx5XhpPjX4SQjHtwEC2xKkWnEv+VPgO1JWdooURcSC6QIDAQAB AoGAM9exRgVPIS4L+Ynohu+AXJBDgfX2ZtEomUIdUGk6i+cg/RaWTFNQh2IOOBn8 ftxwTfjP4HYXBm5Y60NO66klIlzm6ci303IePmjaj8tXQiriaVA0j4hmW+xgnqQX PubFzfnR2eWLSOGChrNFbd3YABC+qttqT6vT0KpFyLdn49ECQQD3zYCpgelb0EBo gc5BVGkbArcknhPwO39coPqKM4csu6cgI489XpF7iMh77nBTIiy6dsDdRYXZM3bq ELTv6K4/AkEA1BwsIZG51W5DRWaKeobykQIB6FqHLW+Zhedw7BnxS8OflYAcSWi4 uGhq0DPojmhsmUC8jUeLe79CllZNP3LU1wJBAIZcoCnI7g5Bcdr4nyxfJ4pkw4cQ S4FT0XAZPR/YZrADo8/SWCWPdFTGSuaf17nL6vLD1zljK/skY5LwshrvUCMCQQDM MY7ehj6DVFHYlt2LFSyhInCZscTencgK24KfGF5t1JZlwt34YaMqjAMACmi/55Fc e7DIxW5nI/nDZrOY+EAjAkA3BHUx3PeXkXJnXjlh7nGZmk/v8tB5fiofAwfXNfL7 bz0ZrT2Caz995Dpjommh5aMpCJvUGsrYCG6/Pbha9NXl -----END RSA PRIVATE KEY-----` pubKey = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNUXO+Qsl1tw+GjrqFajz0ERSE Us1FHSL/+udZRWn1Atw8gz0+tcGqhWChBDeU9gY5sKLEAZnX3FjC/T/IbqeiSM68 kS5vLkzRI84eiJrm3+IieUqIIicsO+WYxQs+JgVx5XhpPjX4SQjHtwEC2xKkWnEv +VPgO1JWdooURcSC6QIDAQAB` domain = "tmail.io" selector = "test" ) func privKeyRSA(tb testing.TB) *rsa.PrivateKey { block, rest := pem.Decode([]byte(privKey)) require.NotNil(tb, block) require.Empty(tb, rest) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) require.NoError(tb, err) return key } var emailBase = "Received: (qmail 28277 invoked from network); 1 May 2015 09:43:37 -0000" + CRLF + "Received: (qmail 21323 invoked from network); 1 May 2015 09:48:39 -0000" + CRLF + "Received: from mail483.ha.ovh.net (b6.ovh.net [213.186.33.56])" + CRLF + " by mo51.mail-out.ovh.net (Postfix) with SMTP id A6E22FF8934" + CRLF + " for ; Mon, 4 May 2015 14:00:47 +0200 (CEST)" + CRLF + "MIME-Version: 1.0" + CRLF + "Date: Fri, 1 May 2015 11:48:37 +0200" + CRLF + "Message-ID: " + CRLF + "Subject: Test DKIM" + CRLF + "From: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= " + CRLF + "To: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= " + CRLF + "Content-Type: text/plain; charset=UTF-8" + CRLF + CRLF + "Hello world" + CRLF + "line with trailing space " + CRLF + "line with space " + CRLF + "-- " + CRLF + "Toorop" + CRLF + CRLF + CRLF + CRLF + CRLF + CRLF var emailBaseNoFrom = "Received: (qmail 28277 invoked from network); 1 May 2015 09:43:37 -0000" + CRLF + "Received: (qmail 21323 invoked from network); 1 May 2015 09:48:39 -0000" + CRLF + "Received: from mail483.ha.ovh.net (b6.ovh.net [213.186.33.56])" + CRLF + " by mo51.mail-out.ovh.net (Postfix) with SMTP id A6E22FF8934" + CRLF + " for ; Mon, 4 May 2015 14:00:47 +0200 (CEST)" + CRLF + "MIME-Version: 1.0" + CRLF + "Date: Fri, 1 May 2015 11:48:37 +0200" + CRLF + "Message-ID: " + CRLF + "Subject: Test DKIM" + CRLF + "To: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= " + CRLF + "Content-Type: text/plain; charset=UTF-8" + CRLF + CRLF + "Hello world" + CRLF + "line with trailing space " + CRLF + "line with space " + CRLF + "-- " + CRLF + "Toorop" + CRLF + CRLF + CRLF + CRLF + CRLF + CRLF var headerSimple = "From: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= " + CRLF + "Date: Fri, 1 May 2015 11:48:37 +0200" + CRLF + "MIME-Version: 1.0" + CRLF + "Received: from mail483.ha.ovh.net (b6.ovh.net [213.186.33.56])" + CRLF + " by mo51.mail-out.ovh.net (Postfix) with SMTP id A6E22FF8934" + CRLF + " for ; Mon, 4 May 2015 14:00:47 +0200 (CEST)" + CRLF + "Received: (qmail 21323 invoked from network); 1 May 2015 09:48:39 -0000" + CRLF var headerRelaxed = "from:=?UTF-8?Q?St=C3=A9phane_Depierrepont?= " + CRLF + "date:Fri, 1 May 2015 11:48:37 +0200" + CRLF + "mime-version:1.0" + CRLF + "received:from mail483.ha.ovh.net (b6.ovh.net [213.186.33.56]) by mo51.mail-out.ovh.net (Postfix) with SMTP id A6E22FF8934 for ; Mon, 4 May 2015 14:00:47 +0200 (CEST)" + CRLF + "received:(qmail 21323 invoked from network); 1 May 2015 09:48:39 -0000" + CRLF var bodySimple = "Hello world" + CRLF + "line with trailing space " + CRLF + "line with space " + CRLF + "-- " + CRLF + "Toorop" + CRLF var bodyRelaxed = "Hello world" + CRLF + "line with trailing space" + CRLF + "line with space" + CRLF + "--" + CRLF + "Toorop" + CRLF var signedRelaxedRelaxed = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;" + CRLF + " s=test; d=tmail.io; h=from:date:mime-version:received:received;" + CRLF + " bh=4pCY+Pp2c/Wr8fDfBDWKpx3DDsr0CJfSP9H1KYxm5bA=;" + CRLF + " b=o0eE20jd8jYqkyxP5rqbfcoUABWZyfrL+l3e1lC0Z+b1Azyrdv+UMmx8L5F57Rhya1SNG2" + CRLF + " 9FnMUTwq+u1PmOmB7NwfTq5UCS9UR8wrNffI1mLUsBPFtv+jZtnHzdmR9aCo2HPfBBALC8" + CRLF + " jEhQcvm/RaP0aiYJtisLJ86S3k0P1WU=" + CRLF + emailBase var signedRelaxedRelaxedLength = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;" + CRLF + " s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=byhiFWd0lAM1sqD1tl8S1DZtKNqgiEZp8jrGds6RRydnZkdX9rCPeL0Q5MYWBQ/JmQrml5" + CRLF + " pIghLwl/EshDBmNy65O6qO8pSSGgZmM3T7SRLMloex8bnrBJ4KSYcHV46639gVEWcBOKW0" + CRLF + " h1djZu2jaTuxGeJzlFVtw3Arf2B93cc=" + CRLF + emailBase var signedSimpleSimple = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=simple/simple;" + CRLF + " s=test; d=tmail.io; h=from:date:mime-version:received:received;" + CRLF + " bh=ZrMyJ01ZlWHPSzskR7A+4CeBDAd0m8CPny4m15ablao=;" + CRLF + " b=nzkqVMlEBL+6m/1AtlFzGV2tHjvfNwFmz9kUDNqphBNSvguv/8KAdqsVheBudJBDHNPrjr" + CRLF + " +N57+atXBQX/jng2WAlI5wpQb1TlxLfm8b7SyS1Z7WwSOI0MqaLMhIss4QEVsevaTF1d/1" + CRLF + " WcFzOPxn66nnn+CRKaz553tjIn1GeFQ=" + CRLF + emailBase var signedSimpleSimpleLength = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=simple/simple;" + CRLF + " s=test; d=tmail.io; l=5; h=from:subject:date:message-id;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=P4cX4WxnSytfsQ3skg3fYIRljleh2iDJidlr/GPfA4S8pTPNZj4SPhB7CJ6OcbSWwJ6Yer" + CRLF + " rHGEmCSEGHJPQm+P12iujJlQ784i34JsBvMC5YAMIQ0DHTNhJRHEyShg1I0B3tqArogdap" + CRLF + " qwWLUSFEhPTXglZVhcHIvYZA9X38iF4=" + CRLF + emailBase var signedNoFrom = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=simple/simple;" + CRLF + " s=test; d=tmail.io; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF + " AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF + " DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBaseNoFrom var signedMissingFlag = "DKIM-Signature: v=1; q=dns/txt; c=simple/simple;" + CRLF + " s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF + " AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF + " DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase var signedBadAFlag = "DKIM-Signature: v=1; a=rsashasha sfds; q=dns/txt; c=simple/simple;" + CRLF + " s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF + " AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF + " DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase var signedBadAlgo = "DKIM-Signature: v=1; a=rsa-shasha; q=dns/txt; c=simple/simple;" + CRLF + " s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF + " AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF + " DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + emailBase var signedDouble = "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=simple/simple;" + CRLF + " s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=SoEhlu1Emm2ASqo8jMhz6FIf2nNHt3ouY4Av/pFFEkQ048RqUFP437ap7RbtL2wh0N3Kkm" + CRLF + " AKF2TcTLZ++1nalq+djU+/aP4KYQd4RWWFBjkxDzvCH4bvB1M5AGp4Qz9ldmdMQBWOvvSp" + CRLF + " DIpJW4XNA/uqLSswtjCYbJsSg9Ywv1o=" + CRLF + "DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;" + CRLF + " s=test; d=tmail.io; l=5; h=from:date:mime-version:received:received;" + CRLF + " bh=GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=;" + CRLF + " b=byhiFWd0lAM1sqD1tl8S1DZtKNqgiEZp8jrGds6RRydnZkdX9rCPeL0Q5MYWBQ/JmQrml5" + CRLF + " pIghLwl/EshDBmNy65O6qO8pSSGgZmM3T7SRLMloex8bnrBJ4KSYcHV46639gVEWcBOKW0" + CRLF + " h1djZu2jaTuxGeJzlFVtw3Arf2B93cc=" + CRLF + emailBase var fromGmail = "Return-Path: toorop@gmail.com" + CRLF + "Delivered-To: toorop@tmail.io" + CRLF + "Received: tmail deliverd local d9ae3ac7c238a50a6e007d207337752eb04038ff; 21 May 2015 19:47:54 +0200" + CRLF + "X-Env-From: toorop@gmail.com" + CRLF + "Received: from 209.85.217.176 (mail-lb0-f176.google.com.) (mail-lb0-f176.google.com)" + CRLF + " by 5.196.15.145 (mail.tmail.io.) with ESMTPS; 21 May 2015 19:47:54 +0200; tmail 0.0.8" + CRLF + " ; 8008e7eae6f168de88db072ead2b34d0f9194cc5" + CRLF + "Authentication-Results: dkim=permfail body hash did not verify" + CRLF + "Received: by lbbqq2 with SMTP id qq2so23551469lbb.3" + CRLF + " for ; Thu, 21 May 2015 10:43:42 -0700 (PDT)" + CRLF + "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;" + CRLF + " d=gmail.com; s=20120113;" + CRLF + " h=mime-version:date:message-id:subject:from:to:content-type;" + CRLF + " bh=pwO8HiXlNND4gOHL7bTlAtJFqYruIH1x8q3dAqEw138=;" + CRLF + " b=lh5rCv0Y2uh23DLUv+YsPZEmJMkhxlVRG+aeCmtJ5BpXTbSHldmNv1vbSegCx0LY9K" + CRLF + " l0AEGrpce6YgBk5qRphffEOhANKEkrLesMUyI3yc9JG2J6R19mJ/NyDkT5USZZuI8DOp" + CRLF + " GkRQSIPU4lrj3U27pr6+8I2lANJfINkqbkbBb69068/aPYl2DUMP5SPCFNwB01LHWKqI" + CRLF + " srRDhqRYnAql+PZJVbzrue2HwBflr4ycDzhfZ+Q5BxQZt+TJtzkCUHTGtx5z9JctR93E" + CRLF + " K5hUpKBN6w6GEbj1HDiMsYZOICx3XNDkny8HhFmU0nPjwbHN2C8HslOGZtDPeZWJypSG" + CRLF + " Wuig==" + CRLF + "MIME-Version: 1.0" + CRLF + "X-Received: by 10.152.206.103 with SMTP id ln7mr3235525lac.40.1432230222503;" + CRLF + " Thu, 21 May 2015 10:43:42 -0700 (PDT)" + CRLF + "Received: by 10.112.162.129 with HTTP; Thu, 21 May 2015 10:43:42 -0700 (PDT)" + CRLF + "Date: Thu, 21 May 2015 19:43:42 +0200" + CRLF + "Message-ID: " + CRLF + "Subject: Test smtpdData" + CRLF + "From: =?UTF-8?Q?St=C3=A9phane_Depierrepont?= " + CRLF + "To: toorop@tmail.io" + CRLF + "Content-Type: text/plain; charset=UTF-8" + CRLF + CRLF + "Alors ?" + CRLF + CRLF + "-- " + CRLF + "Toorop" + CRLF + "http://www.protecmail.com" + CRLF + CRLF + CRLF var missingHeaderMail = "Received: tmail deliverd remote 439903a23facd153908f3e17fb487962d01f4b44; 02 Jun 2015 10:00:24 +0000" + CRLF + "X-Env-From: toorop@toorop.fr" + CRLF + "Received: from 192.168.0.2 (no reverse) by 192.168.0.46 (no reverse) whith" + CRLF + " SMTP; 02 Jun 2015 10:00:23 +0000; tmail 0.0.8;" + CRLF + " d3c348615ef29692ca8bdacb40d0e147c977579c" + CRLF + "Message-ID: <1433239223.d3c348615ef29692ca8bdacb40d0e147c977579c@toorop.fr>" + CRLF + "Date: Thu, 21 May 2015 19:43:42 +0200" + CRLF + "Subject: test" + CRLF + CRLF + "test" func Test_NewSigOptions(t *testing.T) { options := NewSigOptions() assert.Equal(t, "rsa-sha256", options.Algo) assert.Equal(t, "simple/simple", options.Canonicalization) } func Test_SignConfig(t *testing.T) { email := []byte(emailBase) emailToTest := append([]byte(nil), email...) options := NewSigOptions() err := Sign(&emailToTest, options) assert.NotNil(t, err) // && err No private key assert.EqualError(t, err, ErrSignPrivateKeyRequired.Error()) options.PrivateKey = []byte(privKey) emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) // Domain assert.EqualError(t, err, ErrSignDomainRequired.Error()) options.Domain = "toorop.fr" emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) // Selector assert.Error(t, err, ErrSignSelectorRequired.Error()) options.Selector = "default" emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) assert.NoError(t, err) // Canonicalization options.Canonicalization = "simple/relaxed/simple" emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) options.Canonicalization = "simple/relax" emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) assert.EqualError(t, err, ErrSignBadCanonicalization.Error()) options.Canonicalization = "relaxed" emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) assert.NoError(t, err) options.Canonicalization = "SiMple/relAxed" emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) assert.NoError(t, err) // header options.Headers = []string{"toto"} emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) assert.EqualError(t, err, ErrSignHeaderShouldContainsFrom.Error()) options.Headers = []string{"To", "From"} emailToTest = append([]byte(nil), email...) err = Sign(&emailToTest, options) assert.NoError(t, err) } func Test_canonicalize(t *testing.T) { email := []byte(emailBase) emailToTest := append([]byte(nil), email...) options := NewSigOptions() options.Headers = []string{"from", "date", "mime-version", "received", "received", "In-Reply-To"} // simple/simple options.Canonicalization = "simple/simple" header, body, err := canonicalize(&emailToTest, options.Canonicalization, options.Headers) assert.NoError(t, err) assert.Equal(t, []byte(headerSimple), header) assert.Equal(t, []byte(bodySimple), body) // relaxed/relaxed emailToTest = append([]byte(nil), email...) options.Canonicalization = "relaxed/relaxed" header, body, err = canonicalize(&emailToTest, options.Canonicalization, options.Headers) assert.NoError(t, err) assert.Equal(t, []byte(headerRelaxed), header) assert.Equal(t, []byte(bodyRelaxed), body) } func Test_Sign(t *testing.T) { email := []byte(emailBase) emailRelaxed := append([]byte(nil), email...) options := NewSigOptions() options.PrivateKey = []byte(privKey) options.Domain = domain options.Selector = selector // options.SignatureExpireIn = 3600 options.Headers = []string{"from", "date", "mime-version", "received", "received"} options.AddSignatureTimestamp = false options.Canonicalization = "relaxed/relaxed" err := Sign(&emailRelaxed, options) assert.NoError(t, err) assert.Equal(t, []byte(signedRelaxedRelaxed), emailRelaxed) options.BodyLength = 5 emailRelaxed = append([]byte(nil), email...) err = Sign(&emailRelaxed, options) assert.NoError(t, err) assert.Equal(t, []byte(signedRelaxedRelaxedLength), emailRelaxed) options.BodyLength = 0 options.Canonicalization = "simple/simple" emailSimple := append([]byte(nil), email...) err = Sign(&emailSimple, options) assert.Equal(t, []byte(signedSimpleSimple), emailSimple) options.Headers = []string{"from", "subject", "date", "message-id"} memail := []byte(missingHeaderMail) err = Sign(&memail, options) assert.NoError(t, err) options.BodyLength = 5 options.Canonicalization = "simple/simple" emailSimple = append([]byte(nil), email...) err = Sign(&emailSimple, options) assert.Equal(t, []byte(signedSimpleSimpleLength), emailSimple) } func Test_Verify(t *testing.T) { resolveTXT := DNSOptLookupTXT(func(name string) ([]string, error) { switch name { case selector + "._domainkey." + domain: return []string{"v=DKIM1; t=y; p=" + pubKey}, nil // case "TODO._domainkey.gmail.com": // return []string{"v=DKIM1; p="}, nil default: return net.LookupTXT(name) } }) // no DKIM header email := []byte(emailBase) status, err := Verify(&email, resolveTXT) assert.Equal(t, NOTSIGNED, status) assert.Equal(t, ErrDkimHeaderNotFound, err) // No From email = []byte(signedNoFrom) status, err = Verify(&email, resolveTXT) assert.Equal(t, ErrVerifyBodyHash, err) assert.Equal(t, TESTINGPERMFAIL, status) // cause we use dkheader of the "with from" email // missing mandatory 'a' flag email = []byte(signedMissingFlag) status, err = Verify(&email, resolveTXT) assert.Error(t, err) assert.Equal(t, PERMFAIL, status) assert.Equal(t, ErrDkimHeaderMissingRequiredTag, err) // missing bad algo email = []byte(signedBadAlgo) status, err = Verify(&email, resolveTXT) assert.Equal(t, PERMFAIL, status) assert.Equal(t, ErrSignBadAlgo, err) // bad a flag email = []byte(signedBadAFlag) status, err = Verify(&email, resolveTXT) assert.Equal(t, PERMFAIL, status) assert.Equal(t, ErrSignBadAlgo, err) // relaxed email = []byte(signedRelaxedRelaxedLength) status, err = Verify(&email, resolveTXT) assert.NoError(t, err) assert.Equal(t, SUCCESS, status) // simple email = []byte(signedSimpleSimpleLength) status, err = Verify(&email, resolveTXT) assert.NoError(t, err) assert.Equal(t, SUCCESS, status) // gmail // TODO: // Google removed this DNS record some time ago. Someone will have to send an email they're // OK with being publicly available, replace the value of the fromGmail var with that, then grab // the DNS record indicated in the DKIM signature and update the resolveTXT function to return // it when asked. Then this should work. // email = []byte(fromGmail) // status, err = Verify(&email, resolveTXT) // assert.NoError(t, err) // assert.Equal(t, SUCCESS, status) } func Test_SignatureExpiration(t *testing.T) { email := []byte(emailBase) options := NewSigOptions() options.PrivateKey = []byte(privKey) options.Domain = domain options.Selector = selector options.Headers = []string{"from", "date", "mime-version", "received", "received"} options.AddSignatureTimestamp = true options.SignatureExpireIn = 1 // 1 second for testing // Sign the email err := Sign(&email, options) assert.NoError(t, err) // Wait for the signature to expire time.Sleep(2 * time.Second) // Verify the email resolveTXT := DNSOptLookupTXT(func(name string) ([]string, error) { switch name { case selector + "._domainkey." + domain: return []string{"v=DKIM1; t=y; p=" + pubKey}, nil default: return net.LookupTXT(name) } }) status, err := Verify(&email, resolveTXT) assert.Equal(t, TESTINGPERMFAIL, status) assert.Equal(t, ErrVerifySignatureHasExpired, err) } golang-github-toorop-go-dkim-0.0~git20250226.9025cce/errors.go000066400000000000000000000075311476712320300234210ustar00rootroot00000000000000package dkim import ( "errors" ) var ( // ErrSignPrivateKeyRequired when there not private key in config ErrSignPrivateKeyRequired = errors.New("PrivateKey is required") // ErrSignDomainRequired when there is no domain defined in config ErrSignDomainRequired = errors.New("Domain is required") // ErrSignSelectorRequired when there is no Selcteir defined in config ErrSignSelectorRequired = errors.New("Selector is required") // ErrSignHeaderShouldContainsFrom If Headers is specified it should at least contain 'from' ErrSignHeaderShouldContainsFrom = errors.New("header must contains 'from' field") // ErrSignBadCanonicalization If bad Canonicalization parameter ErrSignBadCanonicalization = errors.New("bad Canonicalization parameter") // ErrCandNotParsePrivateKey when unable to parse private key ErrCandNotParsePrivateKey = errors.New("can not parse private key, check format (pem) and validity") // ErrSignBadAlgo Bad algorithm ErrSignBadAlgo = errors.New("bad algorithm. Only rsa-sha1 or rsa-sha256 are permitted") // ErrBadMailFormat unable to parse mail ErrBadMailFormat = errors.New("bad mail format") // ErrBadMailFormatHeaders bad headers format (not DKIM Header) ErrBadMailFormatHeaders = errors.New("bad mail format found in headers") // ErrBadDKimTagLBodyTooShort bad l tag ErrBadDKimTagLBodyTooShort = errors.New("bad tag l or bodyLength option. Body length < l value") // ErrDkimHeaderBadFormat when errors found in DKIM header ErrDkimHeaderBadFormat = errors.New("bad DKIM header format") // ErrDkimHeaderNotFound when there's no DKIM-Signature header in an email we have to verify ErrDkimHeaderNotFound = errors.New("no DKIM-Signature header field found ") // ErrDkimHeaderBTagNotFound when there's no b tag ErrDkimHeaderBTagNotFound = errors.New("no tag 'b' found in dkim header") // ErrDkimHeaderNoFromInHTag when from is missing in h tag ErrDkimHeaderNoFromInHTag = errors.New("'from' header is missing in h tag") // ErrDkimHeaderMissingRequiredTag when a required tag is missing ErrDkimHeaderMissingRequiredTag = errors.New("signature missing required tag") // ErrDkimHeaderDomainMismatch if i tag is not a sub domain of d tag ErrDkimHeaderDomainMismatch = errors.New("domain mismatch") // ErrDkimVersionNotsupported version not supported ErrDkimVersionNotsupported = errors.New("incompatible version") // Query method unsupported errQueryMethodNotsupported = errors.New("query method not supported") // ErrVerifyBodyHash when body hash doesn't verify ErrVerifyBodyHash = errors.New("body hash did not verify") // ErrVerifyNoKeyForSignature no key ErrVerifyNoKeyForSignature = errors.New("no key for verify") // ErrVerifyKeyUnavailable when service (dns) is anavailable ErrVerifyKeyUnavailable = errors.New("key unavailable") // ErrVerifyTagVMustBeTheFirst if present the v tag must be the firts in the record ErrVerifyTagVMustBeTheFirst = errors.New("pub key syntax error: v tag must be the first") // ErrVerifyVersionMusBeDkim1 if présent flag v (version) must be DKIM1 ErrVerifyVersionMusBeDkim1 = errors.New("flag v must be set to DKIM1") // ErrVerifyBadKeyType bad type for pub key (only rsa is accepted) ErrVerifyBadKeyType = errors.New("bad type for key type") // ErrVerifyRevokedKey key(s) for this selector is revoked (p is empty) ErrVerifyRevokedKey = errors.New("revoked key") // ErrVerifyBadKey when we can't parse pubkey ErrVerifyBadKey = errors.New("unable to parse pub key") // ErrVerifyNoKey when no key is found on DNS record ErrVerifyNoKey = errors.New("no public key found in DNS TXT") // ErrVerifySignatureHasExpired when signature has expired ErrVerifySignatureHasExpired = errors.New("signature has expired") // ErrVerifyInappropriateHashAlgo when h tag in pub key doesn't contain hash algo from a tag of DKIM header ErrVerifyInappropriateHashAlgo = errors.New("inappropriate has algorithm") ) golang-github-toorop-go-dkim-0.0~git20250226.9025cce/pubKeyRep.go000066400000000000000000000104151476712320300240060ustar00rootroot00000000000000package dkim import ( "crypto/rsa" "crypto/x509" "encoding/base64" "io/ioutil" "mime/quotedprintable" "net" "strings" ) // PubKeyRep represents a parsed version of public key record type PubKeyRep struct { Version string HashAlgo []string KeyType string Note string PubKey rsa.PublicKey ServiceType []string FlagTesting bool // flag y FlagIMustBeD bool // flag i } // DNSOptions holds settings for looking up DNS records type DNSOptions struct { netLookupTXT func(name string) ([]string, error) } // DNSOpt represents an optional setting for looking up DNS records type DNSOpt interface { apply(*DNSOptions) } type dnsOpt func(*DNSOptions) func (opt dnsOpt) apply(dnsOpts *DNSOptions) { opt(dnsOpts) } // DNSOptLookupTXT sets the function to use to lookup TXT records. // // This should probably only be used in tests. func DNSOptLookupTXT(netLookupTXT func(name string) ([]string, error)) DNSOpt { return dnsOpt(func(opts *DNSOptions) { opts.netLookupTXT = netLookupTXT }) } // NewPubKeyRespFromDNS retrieves the TXT record from DNS based on the specified domain and selector // and parses it. func NewPubKeyRespFromDNS(selector, domain string, opts ...DNSOpt) (*PubKeyRep, verifyOutput, error) { dnsOpts := DNSOptions{} for _, opt := range opts { opt.apply(&dnsOpts) } if dnsOpts.netLookupTXT == nil { dnsOpts.netLookupTXT = net.LookupTXT } txt, err := dnsOpts.netLookupTXT(selector + "._domainkey." + domain) if err != nil { if strings.HasSuffix(err.Error(), "no such host") { return nil, PERMFAIL, ErrVerifyNoKeyForSignature } return nil, TEMPFAIL, ErrVerifyKeyUnavailable } // empty record if len(txt) == 0 { return nil, PERMFAIL, ErrVerifyNoKeyForSignature } // parsing, we keep the first record // TODO: if there is multiple record return NewPubKeyResp(txt[0]) } // NewPubKeyResp parses DKIM record (usually from DNS) func NewPubKeyResp(dkimRecord string) (*PubKeyRep, verifyOutput, error) { pkr := new(PubKeyRep) pkr.Version = "DKIM1" pkr.HashAlgo = []string{"sha1", "sha256"} pkr.KeyType = "rsa" pkr.FlagTesting = false pkr.FlagIMustBeD = false p := strings.Split(dkimRecord, ";") for i, data := range p { keyVal := strings.SplitN(data, "=", 2) val := "" if len(keyVal) > 1 { val = strings.TrimSpace(keyVal[1]) } switch strings.ToLower(strings.TrimSpace(keyVal[0])) { case "v": // RFC: is this tag is specified it MUST be the first in the record if i != 0 { return nil, PERMFAIL, ErrVerifyTagVMustBeTheFirst } pkr.Version = val if pkr.Version != "DKIM1" { return nil, PERMFAIL, ErrVerifyVersionMusBeDkim1 } case "h": p := strings.Split(strings.ToLower(val), ":") pkr.HashAlgo = []string{} for _, h := range p { h = strings.TrimSpace(h) if h == "sha1" || h == "sha256" { pkr.HashAlgo = append(pkr.HashAlgo, h) } } // if empty switch back to default if len(pkr.HashAlgo) == 0 { pkr.HashAlgo = []string{"sha1", "sha256"} } case "k": if strings.ToLower(val) != "rsa" { return nil, PERMFAIL, ErrVerifyBadKeyType } case "n": qp, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(val))) if err == nil { val = string(qp) } pkr.Note = val case "p": rawkey := val if rawkey == "" { return nil, PERMFAIL, ErrVerifyRevokedKey } un64, err := base64.StdEncoding.DecodeString(rawkey) if err != nil { return nil, PERMFAIL, ErrVerifyBadKey } pk, err := x509.ParsePKIXPublicKey(un64) if pk, ok := pk.(*rsa.PublicKey); ok { pkr.PubKey = *pk } case "s": t := strings.Split(strings.ToLower(val), ":") for _, tt := range t { tt = strings.TrimSpace(tt) switch tt { case "*": pkr.ServiceType = append(pkr.ServiceType, "all") case "email": pkr.ServiceType = append(pkr.ServiceType, tt) } } case "t": flags := strings.Split(strings.ToLower(val), ":") for _, flag := range flags { flag = strings.TrimSpace(flag) switch flag { case "y": pkr.FlagTesting = true case "s": pkr.FlagIMustBeD = true } } } } // if no pubkey if pkr.PubKey == (rsa.PublicKey{}) { return nil, PERMFAIL, ErrVerifyNoKey } // No service type if len(pkr.ServiceType) == 0 { pkr.ServiceType = []string{"all"} } return pkr, SUCCESS, nil } golang-github-toorop-go-dkim-0.0~git20250226.9025cce/pubKeyRep_test.go000066400000000000000000000226401476712320300250500ustar00rootroot00000000000000package dkim import ( "testing" "github.com/stretchr/testify/assert" ) func TestPubKeyRep(t *testing.T) { t.Parallel() type testCase struct { Name string Txt string Expect *PubKeyRep VerifyOutput verifyOutput Err error } testCases := []testCase{ { Name: "only required", Txt: "p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "empty record", Txt: "", VerifyOutput: PERMFAIL, Err: ErrVerifyNoKey, }, // v= { Name: "version not first", Txt: "p=" + pubKey + "; v=DKIM1", VerifyOutput: PERMFAIL, Err: ErrVerifyTagVMustBeTheFirst, }, { Name: "wrong version", Txt: "v=DKIM2; p=" + pubKey, VerifyOutput: PERMFAIL, Err: ErrVerifyVersionMusBeDkim1, }, // p= { Name: "no key", Txt: "v=DKIM1", VerifyOutput: PERMFAIL, Err: ErrVerifyNoKey, }, { Name: "key revoked", Txt: "v=DKIM1; p=", VerifyOutput: PERMFAIL, Err: ErrVerifyRevokedKey, }, { Name: "key invalid", Txt: "v=DKIM1; p=badBase64", VerifyOutput: PERMFAIL, Err: ErrVerifyBadKey, }, // h= { Name: "all supported hashes", Txt: "v=DKIM1; h=sha1:sha256; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "sha256 only", Txt: "v=DKIM1; h=sha256; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "sha1 only", Txt: "v=DKIM1; h=sha1; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "unsupported hash", Txt: "v=DKIM1; h=sha512; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "unsupported hash with supported hash", Txt: "v=DKIM1; h=sha256:sha512; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "empty hash list", Txt: "v=DKIM1; h=; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, // k= { Name: "key type rsa", Txt: "v=DKIM1; k=rsa; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "unsupported key type", Txt: "v=DKIM1; k=dsa; p=" + pubKey, VerifyOutput: PERMFAIL, Err: ErrVerifyBadKeyType, }, { Name: "empty key type", Txt: "v=DKIM1; k=; p=" + pubKey, VerifyOutput: PERMFAIL, Err: ErrVerifyBadKeyType, }, // n= { Name: "with note", Txt: "v=DKIM1; n=a note; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, Note: "a note", PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "with note (qp)", Txt: "v=DKIM1; n=a note=3B encoded as quoted printable; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, Note: "a note; encoded as quoted printable", PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "with note (bad qp)", Txt: "v=DKIM1; n=a note =! with invalid quoted printable; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, Note: "a note =! with invalid quoted printable", PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "empty note", Txt: "v=DKIM1; n=; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, // s= { Name: "any service", Txt: "v=DKIM1; s=*; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "email service", Txt: "v=DKIM1; s=email; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"email"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "all services", Txt: "v=DKIM1; s=* : email; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all", "email"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "unsupported service", Txt: "v=DKIM1; s=unknown; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "unsupported service with supported service", Txt: "v=DKIM1; s=unknown:email; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"email"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "empty services", Txt: "v=DKIM1; s=; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, // t= { Name: "testing mode", Txt: "v=DKIM1; t=y; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, FlagTesting: true, }, VerifyOutput: SUCCESS, }, { Name: "strict mode", Txt: "v=DKIM1; t=s; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, FlagIMustBeD: true, }, VerifyOutput: SUCCESS, }, { Name: "both test flags", Txt: "v=DKIM1; t=y : s; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, FlagTesting: true, FlagIMustBeD: true, }, VerifyOutput: SUCCESS, }, { Name: "include invalid test flag", Txt: "v=DKIM1; t=y:s:?; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, FlagTesting: true, FlagIMustBeD: true, }, VerifyOutput: SUCCESS, }, { Name: "invalid test flag", Txt: "v=DKIM1; t=?; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, { Name: "empty test flags", Txt: "v=DKIM1; t=; p=" + pubKey, Expect: &PubKeyRep{ Version: "DKIM1", HashAlgo: []string{"sha1", "sha256"}, KeyType: "rsa", ServiceType: []string{"all"}, PubKey: privKeyRSA(t).PublicKey, }, VerifyOutput: SUCCESS, }, } for _, tc := range testCases { // Subtests are actually run in goroutines, so make sure to capture the loop var tc := tc t.Run(tc.Name, func(t *testing.T) { pubKeyRep, vo, err := NewPubKeyResp(tc.Txt) if tc.Err != nil { assert.EqualError(t, err, tc.Err.Error()) } else { assert.NoError(t, err) } assert.Equal(t, tc.VerifyOutput, vo) assert.EqualValues(t, tc.Expect, pubKeyRep) }) } } golang-github-toorop-go-dkim-0.0~git20250226.9025cce/watch000077500000000000000000000001761476712320300226100ustar00rootroot00000000000000while true do inotifywait -q -r -e modify,attrib,close_write,move,create,delete . && echo "--------------" && go test -v done