DKIM signing

This question is probably for Dave Wyatt :slight_smile:
I built a PS module that has function that acts as SMTP server - sending only. It works fine. It has the benefit of being able to return and subsequently act based on SMTP Reply Codes, as opposed to sending an email via Send-MailMessage which a) depends on an SMTP server, and b) does not return success/failure status/details with respect to the actual communication of the recipient SMTP server.

At any rate, I’m trying to implement Domain Keys Identified Mail (DKIM). I simply have to add a number of tags to the email message header. I’m supposed to performs digital signing on the message, and add the signature information in the email message header. This is my question to you, any insight on how to sign a message, or where to look?

The message can be something like:

From: Sam Boutros  
MIME-Version: 1.0 
To:  
X-Priority: 1 
Priority: urgent 
Importance: high 
Date: 01/30/2015 08:18:01 
Subject: IOPS test 8 
Content-Type: text/html; charset=us-ascii 
Message-ID:  
Here's the IOPS test result:

and I have a private key like:

-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDOiu1LRC5FOS2BBs8XpP3VSAKpTwyjkDOAQoNjebgNUsEo05sy
yrc9ZbWWdHd7hMlWA5RKGvwMzHXs5BesuhfeGkd272fybqlzyT+iOZprryF6iJkj
LF8hvGRO7cLukuRt9ggVZkEu2+Lj+lMD1Ep2355d0UztoiopTIbn12rCmQIDAQAB
AoGAXP1LbLGbq2rcw9SO9HRCG/45xIRkildn+Hz5rpWkecsiUAFFRI7kBO5/3Oc+
zAuyodkmsF6J0DFVfnwK9KcsCvDU6Esy6u35EDLYqXXhQDKLtFrit+pDXyS5K+2D
DGUJ7ASr7AuGl54PP16eHGZC4aA6syLDtoC3iWzcEdY7agECQQD/VfsYltx8cLmB
fhoH4jvepDullBH6OOAnIttXjKKg2syIrFYuJ5xy0tdzgIyuuMR3WfUDLztlX38n
SmfEs2xhAkEAzxR02F4xwR1GUKII5Gz7FQyny1eAYa8RUz6Qu52LVUGDdFWmaPVv
LesO/eqKrq08dBBkdDFVfnwK9KcsCvDU6Esy6u3QJBAIe0G7qKmG0eOPGQWb8b
nh5dwgwqw2aZcQmKn+/3n+nx1X1VP3q8lIh73LcOEWD65ldvVLX+Hn51WaECQQDL
Ud+DzcB2RuMSKnYmqeZA/aMGYQyZXMXbARQkUgRLGj8Si0IPzeNyp1eZ8Z5m8Yro
vmuTU5EuuUz4pDFVfnwK9KcsCvDU6Esy6u3kCGNM9RswLsElnr/
3/kSmyXzc+aUIAlW/DdDmGviYmH5sVZrO3otspQqb6bXmA==
-----END RSA PRIVATE KEY-----

Would have to research the standard first. I think it’s this one: http://www.ietf.org/rfc/rfc6376.txt.

BTW, your private key isn’t exactly private anymore. You might want to generate a new key pair at some point. :wink:

Yep, this is not a real key, it’s made up… :slight_smile:

Here’s how the DKIM header could look like if that helps:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; q=dns/txt; d=smartwebapps.com; s=blabla; h=mime-version:date:message-id:subject:from:to:content-type;
bh=A9Uifui6RO/95G3SBDFU6RO/95G3xcGlwBBoM=;
b=x5m71WJBRBWGw5Xfz2Rm4z4UdAAOTzGRgqiza5nWQCu+I8Il3m+2haJkPYDasJlA9Wwv+MDK5+zKaMQza5nWQCu+I8Il3m+2haJkPYDasJlA9WwvNNwKdY3Ue0r3uN8tl0R2qia4v/OcHO4P2c/yK5E34GiI=

also fake but representative data…

I’m not sure how soon I would have time to review that standard. However, I can offer a few observations from what I’ve seen so far:

  • The .NET Framework crypto libraries are not going to be much help to you at the moment, because they all work with Certificates rather than simple key pairs. You’ll have a much easier time if you use a respected third-party crypto library; perhaps BouncyCastle. Here’s an example of importing those key files with BouncyCastle: encryption - Read RSA PrivateKey in C# and Bouncy Castle - Stack Overflow

And an example of using BouncyCastle’s APIs to generate an RSA signature: C# Sign Data with RSA using BouncyCastle - Stack Overflow

That aside, all that should be left is figuring out exactly what to hash, which algorithms to use, and how to create your DKIM-Signature header.

I haven’t read through all of the standard yet, but based on some educated guesswork of your example header: looks like the header contains the algorithm, so you can use whatever you like. In your example header, I suspect that the “bh=” portion is a SHA256 hash of the message body, and “b=” is the RSA signature of that hash. The c,q,d, and possible s portions of the header probably refer to the DNS query that is needed in order to download the public key that can be used to verify the signature.

Dave,
Thank you for taking the time to look into this. I truly appreciate it.

I downloaded the BouncyCastle DLL, and loaded the assembly in a PS script

[Reflection.Assembly]::LoadFile('C:\Sandbox\BouncyCastle.Crypto.dll')

I can see classes using cmdlets like:

$DLL = 'C:\Sandbox\BouncyCastle.Crypto.dll'
$Classes = [appdomain]::currentdomain.GetAssemblies() | where { $_.Location -eq $DLL } 
$Classes.GetTypes() | sort basetype
$Classes.GetTypes().Name -match 'object' | sort
$Classes.GetTypes().Name -match 'BigInteger'
$Classes.GetTypes() | where { $_.Name -match 'Signer' } # SignerUtilities

I suppose my next step is to create and use objects with New-Object but I’m having hard time identifying the object types I need to use based on the C# examples above. I have not used C# before.

I played around with it a bit. Here’s some basic code to load up an OpenSSH private RSA key file and use it to sign something. (In this case, I’m signing the byte array 1…32, but you’d probably be using it to sign the hash of your message body or something.)

For giggles, I also included some code to verify the signature, though you may not necessarily care about that part in your project.

$pemFileName = 'C:\Path\To\Private\Key\file.pem'
$dataToSign = [byte[]](1..32)

$fileStream = [System.IO.File]::OpenText($pemFileName)
$pemReader = New-Object Org.BouncyCastle.OpenSsl.PemReader($fileStream)
$keyPair = $pemReader.ReadObject()

$signer = [Org.BouncyCastle.Security.SignerUtilities]::GetSigner('RSA')
$signer.Init($true, $keyPair.Private)
$signer.BlockUpdate($dataToSign, 0, $dataToSign.Count)
$signature = $signer.GenerateSignature()



$verifier = [Org.BouncyCastle.Security.SignerUtilities]::GetSigner('RSA')
$verifier.Init($false, $keyPair.Public)
$verifier.BlockUpdate($dataToSign, 0, $dataToSign.Count)
$verifier.VerifySignature($signature)

As for computing the hash of a string, I’d just stick with the .NET framework classes for that, as they seem to involve a lot fewer steps than the BouncyCastle classes. For example:

$string = 'I am a string.'
$bytes = [System.Text.Encoding]::UTF8.GetBytes($string)
$sha = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider
$hash = $sha.ComputeHash($bytes)

For the heck of it, I started researching how to make this work without Bouncy Castle. Turns out that the CryptoAPI already has the ability to import DER-encoded key files via the CryptDecodeObject function, and you can tell an RsaCryptoServiceProvider object to use that key info.

However, this is extremely limited. It doesn’t handle password-protected private key files, and writing the code to read those headers and decrypt the file first would be more work, so I’d still use Bouncy Castle in practice. I was just curious if I could get it working. :slight_smile:

function Get-RsaFromPemFile
{
    param (
        [Parameter(Mandatory)]
        [string] $Path,

        [switch] $Private
    )

    Add-Type -TypeDefinition @'
        using System;
        using System.Runtime.InteropServices;

        public class CryptoWrapper
        {
            [DllImport("Crypt32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool CryptDecodeObject(int encodingType,
                                                        int structType,
                                                        byte[] encoded,
                                                        int encodedCount,
                                                        int flags,
                                                        byte[] blob,
                                                        ref int blobSize);
        }
'@
    
    $rsa_private_key_type = 43

    $x509_asn_encoding_constant = 1
    $pkcs7_asn_encoding_constant = 65536

    $encodingType = $x509_asn_encoding_constant -bor $pkcs7_asn_encoding_constant

    $count = 0

    $pem = [string[]]@(Get-Content $Path)
    $base64 = $pem -match '^[a-z0-9\+/=]+$' -join ''"
    $bytes = [convert]::FromBase64String($base64)

    $success = [CryptoWrapper]::CryptDecodeObject($encodingType, $rsa_private_key_type, $bytes, $bytes.Count, 0, $null, [ref] $count)

    if ($success)
    {
        $blob = New-Object byte[]($count)
        $success = [CryptoWrapper]::CryptDecodeObject($encodingType, $rsa_private_key_type, $bytes, $bytes.Count, 0, $blob, [ref] $count)

        if ($success)
        {
            $rsaProvider = New-Object System.Security.Cryptography.RSACryptoServiceProvider
            $rsaProvider.ImportCspBlob($blob)

            return $rsaProvider
        }
    }

    $errCode = [int][System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
    $win32ex = New-Object System.ComponentModel.Win32Exception($errCode)

    throw "Error decoding PEM file: $errCode, $($win32ex.Message)"
}

$rsaProvider = Get-RsaFromPemFile -Path C:\Users\dlwya_000\desktop\test.pem -Private

$toSign = [byte[]](1..32)
$sha256provider = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider
$hash = $sha256provider.ComputeHash($toSign)

$sha256Oid = [System.Security.Cryptography.CryptoConfig]::MapNameToOID('SHA256')

$signature = $rsaProvider.SignHash($hash, $sha256Oid)

$rsaProvider.VerifyHash($hash, $sha256Oid, $signature)

This seems to work:

#Input
$From         = 'FirstNameLastName@clientdomain.com' 
$SenderName   = 'FirstName LastName via clientdomain.com'
$DKIMDomain   = 'SendingCompanyDomain'
$DKIMSelector = 'ClientDomain'
$To           = 'samb@gmail.com'
$Subject      = 'My message subject line'
$MyFQDN       = 'mail.SendingCompanyDomain.com'
$Body         = @"
Here's the IOPS test result:
"@ # Build SMTP body $Command = "From: $SenderName `r`n" $Command += "MIME-Version: 1.0 `r`n" $Command += "To: $To `r`n" $Command += "X-Priority: 1 `r`n" $Command += "Priority: urgent `r`n" $Command += "Importance: high `r`n" $Command += "Date: $(Get-Date) `r`n" $Command += "Subject: $Subject `r`n" $Command += "Content-Type: text/html; charset=us-ascii `r`n" $Command += "Message-ID: `r`n" $Command += "$Body `r`n" # Compute the hash of $Command (String) $Bytes = [System.Text.Encoding]::UTF8.GetBytes($Command) $Sha = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider $Hash = $Sha.ComputeHash($Bytes) # Load the BouncyCastle assembly, read the private key [Reflection.Assembly]::LoadFile('C:\Sandbox\BouncyCastle.Crypto.dll') | Out-Null $pemFileName = 'C:\Sandbox\Privatekeyfile.DKIM' $FileStream = [System.IO.File]::OpenText($pemFileName) $pemReader = New-Object Org.BouncyCastle.OpenSsl.PemReader($FileStream) $KeyPair = $pemReader.ReadObject() $FileStream.Close() # Sign the message ($Command) $Signer = [Org.BouncyCastle.Security.SignerUtilities]::GetSigner('RSA') $Signer.Init($true, $KeyPair.Private) $Signer.BlockUpdate($Hash, 0, $Hash.Count) $Signature = $Signer.GenerateSignature() # Build the DKIM header $DKIMHeader = 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; q=dns/txt; ' $DKIMHeader += "d=$DKIMDomain; s=$DKIMSelector; " $DKIMHeader += 'h=mime-version:date:message-id:subject:from:to:content-type; ' $DKIMHeader += "bh=$([System.Convert]::ToBase64String($Hash)); " $DKIMHeader += "b=$([System.Convert]::ToBase64String($Signature)) `r`n" # Finally, assemble the message adding the DKIM header $Command = $DKIMHeader + $Command $Command