【问题标题】:DKIM - body hash did not verifyDKIM - 正文哈希未验证
【发布时间】:2021-03-07 14:18:30
【问题描述】:

我正在尝试使用 DKIM 标头向 gmail(或任何其他电子邮件提供商)发送一封非常简单的电子邮件。

gmail中的结果是:dkim=neutral(body hash没有验证)

我认为正文散列不正确。 我把body弄得超级简单,但还是报同样的错误。

这是 SMTP 数据字符串:

DKIM-Signature:v=1; a=rsa-sha1; q=dns/txt; s=default;\r\n c=simple/simple; d=cumulo9.com; h=Date:From:To:Content-Type:Content-Transfer-Encoding;\r\n t=1489977499; bh=rtE3fSBFa/HdaPcuGaMM2mZVL7Mljo9sPTNOBjmNBdgIpGYh+ukt71Joc/qFd/nY70yn/hW0nASN+SZARGY2ri0ymA6NUrCIcSX7yJxJ6MkO78cyGZUoHY6Y+kOsDfCUcH5ANHJs88iUtu4IviWP4vWHXBd/tqP9k7Q+UKaC+m4=;\r\n b=klwC+c8qFKVD32SK22K04/YID+TerTvd26+VnlTljNA3fOEVbi2YlvTFo5LM1VksmO08hu5iJfwmF/3GgSEOnGT3mrzXxofjPbvIWU181zluxObNt8FwrP0kCIUskJEQz2SPF1VzaMQ8QvVchnkEFYrW9Pvssk6hunNr8J6CGrc=\r\nDate: Mon, 20 Mar 2017 15:38:17 +1300\r\nFrom: <leo@cumulo9.com>\r\nTo: leo@cumulo9.com\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nhelloleo\r\n.

我唯一能想到的就是body hash code一定有错误。

public string SignBody(string body)
    {
        var cb = body + "\r\n";

        IPrivateKeySigner _privateKeySigner = new MailPost.DKIM.PrivateKeySigner(PrivateKey);

        byte[] defaultEncoding = Encoding.UTF8.GetBytes(cb);

        byte[] hash = _privateKeySigner.Sign(defaultEncoding, SigningAlgorithm.RSASha1);

        string bodyHash = Convert.ToBase64String(hash);

        return bodyHash;
    }

“PrivateKeySigner”类中的函数:

public byte[] Sign(byte[] data, SigningAlgorithm algorithm)
    {
        if (data == null)
        {
            throw new ArgumentNullException("data");
        }

        using (var rsa = OpenSslKey.DecodeRSAPrivateKey(m_key))
        {
            byte[] signature = rsa.SignData(data, GetHashName(algorithm));

            return signature;

        }
    }

“OpenSslKey”类中的函数:

public static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey)
    {
        if (privkey == null)
        {
            throw new ArgumentNullException("privkey");
        }

        byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;

        // ---------  Set up stream to decode the asn.1 encoded RSA private key  ------
        //var mem = new MemoryStream(privkey);
        using (var binr = new BinaryReader(new MemoryStream(privkey)))    //wrap Memory Stream with BinaryReader for easy reading
        {

            ushort twobytes = binr.ReadUInt16();
            if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                binr.ReadByte(); //advance 1 byte
            else if (twobytes == 0x8230)
                binr.ReadInt16(); //advance 2 bytes
            else
                return null;

            twobytes = binr.ReadUInt16();
            if (twobytes != 0x0102) //version number
                return null;
            byte bt = binr.ReadByte();
            if (bt != 0x00)
                return null;


            //------  all private key components are Integer sequences ----
            int elems = GetIntegerSize(binr);
            MODULUS = binr.ReadBytes(elems);

            elems = GetIntegerSize(binr);
            E = binr.ReadBytes(elems);

            elems = GetIntegerSize(binr);
            D = binr.ReadBytes(elems);

            elems = GetIntegerSize(binr);
            P = binr.ReadBytes(elems);

            elems = GetIntegerSize(binr);
            Q = binr.ReadBytes(elems);

            elems = GetIntegerSize(binr);
            DP = binr.ReadBytes(elems);

            elems = GetIntegerSize(binr);
            DQ = binr.ReadBytes(elems);

            elems = GetIntegerSize(binr);
            IQ = binr.ReadBytes(elems);


            // ------- create RSACryptoServiceProvider instance and initialize with public key -----
            var RSA = new RSACryptoServiceProvider();
            var RSAparams = new RSAParameters
                                {
                                    Modulus = MODULUS,
                                    Exponent = E,
                                    D = D,
                                    P = P,
                                    Q = Q,
                                    DP = DP,
                                    DQ = DQ,
                                    InverseQ = IQ
                                };
            RSA.ImportParameters(RSAparams);
            return RSA;

        }
    }

GetIntegerSize() 的代码:

private static int GetIntegerSize([NotNull]BinaryReader binr)
    {
        if (binr == null)
        {
            throw new ArgumentNullException("binr");
        }

        int count;
        byte bt = binr.ReadByte();
        if (bt != 0x02)     //expect integer
            return 0;
        bt = binr.ReadByte();

        if (bt == 0x81)
            count = binr.ReadByte();    // data size in next byte
        else
            if (bt == 0x82)
            {
                byte highbyte = binr.ReadByte();
                byte lowbyte = binr.ReadByte();
                byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
                count = BitConverter.ToInt32(modint, 0);
            }
            else
            {
                count = bt;     // we already have the data size
            }



        while (binr.ReadByte() == 0x00)
        {   //remove high order zeros in data
            count -= 1;
        }
        binr.BaseStream.Seek(-1, SeekOrigin.Current);       //last ReadByte wasn't a removed zero, so back up a byte
        return count;
    }

目前我真的被这个问题困住了,不知道我做错了什么。由于您项目的性质,我无法使用其他库,例如“MimeKit”。如果您需要有关此问题的更多信息,请告诉我,我会尽力为您提供。

感谢大家帮助我。

【问题讨论】:

  • 为什么不能使用 MimeKit?项目的本质是什么让你无法使用? MimeKit 是 MIT 许可的,这意味着您可以在专有软件或开源软件中使用它。没有许可费。它适用于 .NET Core。可能是什么问题?
  • 我们现有的软件已经在发送电子邮件时提供了它所需要的所有功能,并且经过精心开发,可以完全按照我们的意愿行事。我不允许仅仅因为我对 DKIM 有问题而将所有这些都更改为包括 MimeKit。你能帮我解决如何实现 DKIM 而不是使用 MimeKit 的问题吗?
  • MimeKit 的设计目的是让您无需使用 MailKit 即可使用它(即您可以将它与您自己的 SMTP 库一起使用)——所以我猜您的意思是它不是由于MimeKit 的性质,而是由于 您的 项目的性质 :)
  • GetIntegerSize(BinaryReader) 的代码是多少?
  • 我已将问题更改为包含 GetIntegerSize(BinaryReader) 函数。

标签: c# smtp dkim


【解决方案1】:

你很可能有几个问题(在这里你以为你只有 1 个!)。

首先,您是如何确定邮件的body 的?这只是您在MailMessage 上设置的字符串吗?如果是这样,除非您只发送非常简单的短信(即使那样......它也可能不会,这取决于MailMessage 是否决定它需要使用例如@ 对您的文本进行编码987654327@ 或 quoted-printable 编码)。您需要确保在生成正文哈希之前应用Content-Transfer-Encoding

其次,您是否记得使用rfc6376, section 3.4.3 中描述的Simple 正文规范化规则来转换正文文本?在我看来,您只是在添加"\r\n",但这不是规则所说的。

如果您不想重新发明轮子,可以尝试使用像 MimeKit 这样的库来构建和 DKIM 签名您的消息,如下所示:

var message = new MimeMessage ();
message.From.Add (new MailboxAddress ("", "leo@cumulo9.com"));
message.To.Add (new MailboxAddress ("", "leo@cumulo9.com"));
message.Body = new TextPart ("plain") { Text = "helloleo" };

var headers = new HeaderId[] { HeaderId.Date, HeaderId.From, HeaderId.To, HeaderId.ContentType, HeaderId.ContentTransferEncoding };
var headerAlgorithm = DkimCanonicalizationAlgorithm.Simple;
var bodyAlgorithm = DkimCanonicalizationAlgorithm.Simple;
var signer = new DkimSigner ("privatekey.pem", "cumulo9.com", "default") {
    SignatureAlgorithm = DkimSignatureAlgorithm.RsaSha1,
    QueryMethod = "dns/txt",
};

// Prepare the message body to be sent over a 7bit transport (such as
// older versions of SMTP).
// Note: If the SMTP server you will be sending the message over supports
// the 8BITMIME extension, then you can use `EncodingConstraint.EightBit`
// instead, although it never hurts to use `SevenBit`.
message.Prepare (EncodingConstraint.SevenBit);

message.Sign (signer, headers, headerAlgorithm, bodyAlgorithm);

// to write out the message so you have something to compare with:
var options = FormatOptions.Default.Clone ();
options.NewLineFormat = NewLineFormat.Dos;

message.WriteTo (options, "message.txt");

然后,一旦您拥有 DKIM 签名的邮件,您就可以使用 MailKit 通过 SMTP 发送它,如下所示:

using (var client = new SmtpClient ()) {
    // For demo-purposes, accept all SSL certificates
    client.ServerCertificateValidationCallback = (s,c,h,e) => true;

    client.Connect ("smtp.gmail.com", 587, SecureSocketOptions.StartTls);

    // Note: since we don't have an OAuth2 token, disable
    // the XOAUTH2 authentication mechanism.
    client.AuthenticationMechanisms.Remove ("XOAUTH2");

    // Note: only needed if the SMTP server requires authentication
    client.Authenticate ("joey@gmail.com", "password");

    client.Send (message);
    client.Disconnect (true);
}

【讨论】:

  • 我又看了一遍文章 '3.4.3. “简单”的身体规范化算法',但我没有从中得到任何更明智的结果。 “使用 rfc6376 第 3.4.3 节中描述的简单正文规范化规则转换正文文本是什么意思。在我看来,您只是在添加“\r\n”,但这不是规则所说的做。”?规则谈论空白和空行。在我的身体里,我只有这个“helloleo”。根本没有空行或空格。 "\r\n" 是纯文本中的换行符。你能解释一下你的意思吗?
  • 当我创建 DKIM bh(内容散列)时,我正在散列此内容“helloleo”(也尝试使用换行符“helloleo\r\n”)并使用 Content-Transfer -编码类型为“7bit”。那么这应该是我认为的工作。因此,我认为我对这些内容进行哈希处理的方式一定有问题?
  • 抱歉,我错过了您的原始问题包含 MIME 消息的全部内容(我认为问题仅包含原始 DKIM-Signature 标头)。所以是的,因为您的签名是在"helloleo\r\n" 上执行的,所以应该可以工作(注意:您需要"\r\n")。我还假设原始数据末尾的 '.' 是 SMTP DATA 命令的终止序列的一部分,对吧?
  • FWIW,我说您的代码没有正确规范化要签名的内容的原因是您的代码所做的只是附加"\r\n",这不是一般意义上的规范化方式。在您非常基本的测试用例中,是的,这就是您需要做的事情,但是一旦您开始提供此函数 real world 消息体,这不是您以后需要做的事情。
  • 建议:下载一份 MimeKit 副本,复制我上面的代码(确保加载正确的私钥文件),然后将 MimeKit 生成的正文哈希与您的代码生成的正文哈希进行比较。
【解决方案2】:

我本来会发表评论,但我的声誉还不够高。 :/

我正在使用 PHPMailer,但如果我的解决方案更普遍适用,我想我会留下一个便条。

在我已经在测试环境中验证了我的电子邮件系统和 DKIM 密钥之后,我的电子邮件触发了 dkim=neutral (body hash did not verify) 错误。

事实证明,至少对于 PHPMailer,如果 $body 字符串的末尾有一个尾随空格,它被输入$mail-&gt;Body = $body;,那么 DKIM 正文哈希将不匹配,从而触发错误。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    • 2018-02-03
    • 2013-10-30
    • 1970-01-01
    • 2012-08-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多