DKIM signing

This topic contains 9 replies, has 2 voices, and was last updated by Profile photo of Sam Boutros Sam Boutros 1 year, 10 months ago.

  • Author
    Posts
  • #22269
    Profile photo of Sam Boutros
    Sam Boutros
    Participant

    This question is probably for Dave Wyatt 🙂
    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 [b]performs digital signing on the message[/b], 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-----
    

  • #22270
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

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

  • #22271
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

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

    • #22273
      Profile photo of Sam Boutros
      Sam Boutros
      Participant

      Yep, this is not a real key, it's made up.. 🙂

  • #22274
    Profile photo of Sam Boutros
    Sam Boutros
    Participant

    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..

  • #22276
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    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: http://stackoverflow.com/questions/15629551/read-rsa-privatekey-in-c-sharp-and-bouncy-castle

    And an example of using BouncyCastle's APIs to generate an RSA signature: http://stackoverflow.com/questions/8830510/c-sharp-sign-data-with-rsa-using-bouncycastle

    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.

  • #22286
    Profile photo of Sam Boutros
    Sam Boutros
    Participant

    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.

  • #22287
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    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)
    
  • #22288
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    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. 🙂

    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)
    
  • #22290
    Profile photo of Sam Boutros
    Sam Boutros
    Participant

    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

You must be logged in to reply to this topic.