Sign prepends a DKIM-Signature header to the given RFC 5322 message body, signed with the supplied private key for "{selector}.{domain}". We only sign the From, To, Subject, Date and Message-ID headers (plus any References / In-Reply-To that may be present). Signing every header is brittle — receiv
(message []byte, domain, selector string, privateKeyDER []byte)
| 93 | // MTAs rewrite or fold a covered header. The whitelist keeps DMARC |
| 94 | // alignment intact while tolerating typical Send-via-SES rewrites. |
| 95 | func Sign(message []byte, domain, selector string, privateKeyDER []byte) ([]byte, error) { |
| 96 | if domain == "" || selector == "" { |
| 97 | return nil, fmt.Errorf("dkim: domain and selector required") |
| 98 | } |
| 99 | if len(privateKeyDER) == 0 { |
| 100 | return nil, fmt.Errorf("dkim: empty private key") |
| 101 | } |
| 102 | key, err := x509.ParsePKCS1PrivateKey(privateKeyDER) |
| 103 | if err != nil { |
| 104 | return nil, fmt.Errorf("parse private key: %w", err) |
| 105 | } |
| 106 | |
| 107 | opts := &msgauth.SignOptions{ |
| 108 | Domain: domain, |
| 109 | Selector: selector, |
| 110 | Signer: key, |
| 111 | HeaderCanonicalization: msgauth.CanonicalizationRelaxed, |
| 112 | BodyCanonicalization: msgauth.CanonicalizationRelaxed, |
| 113 | HeaderKeys: []string{ |
| 114 | "From", "To", "Cc", "Subject", "Date", |
| 115 | "Message-ID", "In-Reply-To", "References", |
| 116 | "MIME-Version", "Content-Type", "Reply-To", |
| 117 | }, |
| 118 | } |
| 119 | |
| 120 | var signed bytes.Buffer |
| 121 | if err := msgauth.Sign(&signed, bytes.NewReader(message), opts); err != nil { |
| 122 | return nil, fmt.Errorf("dkim sign: %w", err) |
| 123 | } |
| 124 | return signed.Bytes(), nil |
| 125 | } |
| 126 | |
| 127 | // DNSRecord renders the TXT record the user must publish. Returns the |
| 128 | // hostname (left of the apex) and the record value. |