I'm trying to send a very simple email to gmail (or any other email provider) with DKIM header.
The result in gmail is: dkim=neutral (body hash did not verify)
I assume that the body hash isn't correct. I made the body super simple and still get the same error.
Here is the SMTP data string:
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.
The only thing that I can come up with is that there must be an error in the 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;
}
Function in the "PrivateKeySigner" class:
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;
}
}
Function in the "OpenSslKey" class:
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;
}
}
Code for 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;
}
Currently I'm really stuck with this problem and have no idea what I'm doing wrong. I'm not able to use other libraries like "MimeKit" because of the nature of you project. If you need any more information about this problem then please let me know and I will do my best to provide it to you.
Thanks heaps for helping me out.
Most likely you have several problems (and here you thought you only had 1!).
First, how did you determine the body
of the message? Is this just the string that you are setting on the MailMessage
as its body? If so, that's not likely to work unless you are only sending very simple text messages (and even then... it might not, depending on whether or not MailMessage
decides it needs to encode your text using, for example, the base64
or quoted-printable
encoding). You need to make sure that the Content-Transfer-Encoding
gets applied before you generate the body hash.
Secondly, did you remember to transform the body text using the Simple
body canonicalization rules described in rfc6376, section 3.4.3? It looks to me like you are just tacking on a "\r\n"
but that's not what the rules say to do.
If you don't want to reinvent the wheel, you can try using a library like MimeKit to construct and DKIM-sign your message like so:
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");
Then, once you have your DKIM-signed message, you can send it over SMTP using MailKit like so:
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);
}