2

I am attempting to create a PowerShell script that will:

  • Build a message
  • Sign the message using my private S/MIME certificate
  • Encrypt the message using the S/MIME public cert of the recipient
  • Send the email that has been signed and encrypted

I have included the full script below but changed email addresses, cert names, etc.

The private cert has been imported onto the machine using Internet Explorer. It is then referenced within the directory C:\Users\xxx\AppData\Roaming\Microsoft\SystemCertificates\My\Certificates\

The problem is that when I send the email using the script it is being encrypted but not signed.

However, if I don't encrypt the message and instead include $SignedMessageBytes when building the memory stream (see first line in step 4 of script) the email is signed correctly when being sent. This would suggest that the message is being correctly signed before the encryption occurs.

For some reason the script won't include the signature when encrypting the message.

What must I do so that the signature is included when the message is encrypted?

$SMTPServer = "localhost"
$Recipient = "recipient@emailaddress.com"
$From = "sender@emailaddress.com"
$RecipientCertificatePath = "C:\recipient@emailaddress.com.cer"
$SignerCertificatePath = "C:\Users\xxx\AppData\Roaming\Microsoft\SystemCertificates\My\Certificates\xxxx"

Add-Type -assemblyName "System.Security"
$MailClient = New-Object System.Net.Mail.SmtpClient $SMTPServer
$Message = New-Object System.Net.Mail.MailMessage
$Message.To.Add($Recipient)
$Message.From = $From
$Body = $null
$File= get-item -Path "C:\CONTRL__9911837000009_4045399000008_20170704_ELE00207.TXT"
$Message.Subject = $File.Name

# STEP 1: Capture Message Body
$MIMEMessage = New-Object system.Text.StringBuilder
$MIMEMessage.AppendLine("MIME-Version: 1.0") | Out-Null
$MIMEMessage.AppendLine("Content-Type: multipart/mixed; boundary=unique-boundary-1") | Out-Null
$MIMEMessage.AppendLine() | Out-Null
$MIMEMessage.AppendLine("This is a multi-part message in MIME format.") | Out-Null
$MIMEMessage.AppendLine("--unique-boundary-1") | Out-Null
$MIMEMessage.AppendLine("Content-Type: text/plain") | Out-Null
$MIMEMessage.AppendLine("Content-Transfer-Encoding: 7Bit") | Out-Null
$MIMEMessage.AppendLine() | Out-Null
$MIMEMessage.AppendLine($Body) | Out-Null
$MIMEMessage.AppendLine() | Out-Null
$MIMEMessage.AppendLine("--unique-boundary-1") | Out-Null
$MIMEMessage.AppendLine("Content-Type: application/octet-stream; name="+ $file.Name) | Out-Null
$MIMEMessage.AppendLine("Content-Transfer-Encoding: base64") | Out-Null
$MIMEMessage.AppendLine("Content-Disposition: attachment; filename="+ $file.Name) | Out-Null
$MIMEMessage.AppendLine() | Out-Null

[Byte[]] $binaryData = [System.IO.File]::ReadAllBytes($File)
[string] $base64Value = [System.Convert]::ToBase64String($binaryData, 0, $binaryData.Length)
[int] $position = 0
while($position -lt $base64Value.Length)
{
    [int] $chunkSize = 100
    if (($base64Value.Length - ($position + $chunkSize)) -lt 0)
    {
        $chunkSize = $base64Value.Length - $position
    }
    $MIMEMessage.AppendLine($base64Value.Substring($position, $chunkSize)) | Out-Null
    $MIMEMessage.AppendLine() | Out-Null
    $position += $chunkSize;
}

$MIMEMessage.AppendLine("--unique-boundary-1--") | Out-Null
[Byte[]] $MessageBytes = [System.Text.Encoding]::ASCII.GetBytes($MIMEMessage.ToString())


# STEP 2: Sign
$ci = New-Object System.Security.Cryptography.Pkcs.ContentInfo(,$MessageBytes)
$signedCms = New-Object System.Security.Cryptography.Pkcs.SignedCms($ci)

$SignerCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($SignerCertificatePath)
$Signer = New-Object System.Security.Cryptography.Pkcs.CmsSigner( $SignerCertificate )
$timeAttribute = New-Object -TypeName System.Security.Cryptography.Pkcs.Pkcs9SigningTime
$null = $signer.SignedAttributes.Add($timeAttribute)
$sha2_oid = New-Object System.Security.Cryptography.Oid("2.16.840.1.101.3.4.2.1")
$Signer.DigestAlgorithm = $sha2_oid

Write-Host "-----------------------------------------------------"
Write-Host "Cert friendly name: " $Signer.Certificate.FriendlyName
Write-Host "Cert subject      : " $Signer.Certificate.Subject
Write-Host "Cert thumbprint   : " $Signer.Certificate.Thumbprint
Write-Host "Digest algorithm  : " $Signer.DigestAlgorithm.FriendlyName
Write-Host "Sign Time         : " $Signer.SignedAttributes.Values.SigningTime

$signedCms.ComputeSignature($Signer)
$SignedMessageBytes = $signedCms.Encode()


# STEP 3: Encrypt
$ContentInfo = New-Object System.Security.Cryptography.Pkcs.ContentInfo (,$SignedMessageBytes)
$CMSRecipient = New-Object System.Security.Cryptography.Pkcs.CmsRecipient $RecipientCertificatePath
$algo_id = New-Object System.Security.Cryptography.Pkcs.AlgorithmIdentifier("2.16.840.1.101.3.4.1.42")
$EnvelopedCMS = New-Object System.Security.Cryptography.Pkcs.EnvelopedCms( $ContentInfo , $algo_id )

$EnvelopedCMS.Encrypt($CMSRecipient)

Write-Host "Key length       : " $EnvelopedCMS.ContentEncryptionAlgorithm.KeyLength
Write-Host "OID friendly name: " $EnvelopedCMS.ContentEncryptionAlgorithm.Oid.FriendlyName
Write-Host "OID value        : " $EnvelopedCMS.ContentEncryptionAlgorithm.Oid.Value
Write-Host "Parameters       : " $EnvelopedCMS.ContentEncryptionAlgorithm.Parameters

[Byte[]] $EncryptedBytes = $EnvelopedCMS.Encode()


# STEP 4: Create and send mail
$MemoryStream = New-Object System.IO.MemoryStream @(,$EncryptedBytes)
$AlternateView = New-Object System.Net.Mail.AlternateView($MemoryStream, "application/x-pkcs7-mime; smime-type=enveloped-data;name=smime.p7m")
$Message.AlternateViews.Add($AlternateView)
$MailClient.Send($Message)
gerard
  • 835
  • 11
  • 27

1 Answers1

2

Thanks for your groundwork.

I got it working by adding an additional mime layer:

# STEP 3: Encrypt
$OID = New-Object System.Security.Cryptography.Oid 2.16.840.1.101.3.4.1.42
$AId = New-Object System.Security.Cryptography.Pkcs.AlgorithmIdentifier ($OID, 256)

$SignatureBytes = $SignedCMS.Encode()
$MIMEMessage2 = New-Object system.Text.StringBuilder 
$MIMEMessage2.AppendLine('Content-Type: application/pkcs7-mime; smime-type=enveloped-data;name=smime.p7m') | Out-Null 
$MIMEMessage2.AppendLine('Content-Transfer-Encoding: base64') | Out-Null 
$MIMEMessage2.AppendLine() | Out-Null 
$MIMEMessage2.AppendLine([Convert]::ToBase64String($SignedMessageBytes)) | Out-Null

Byte[]] $BodyBytes = [System.Text.Encoding]::UTF8.GetBytes($MIMEMessage2.ToString())

ContentInfo = New-Object System.Security.Cryptography.Pkcs.ContentInfo (,$BodyBytes)

$CMSRecipient = New-Object System.Security.Cryptography.Pkcs.CmsRecipient $ChosenCertificate 
$EnvelopedCMS = New-Object System.Security.Cryptography.Pkcs.EnvelopedCms( $ContentInfo, $AId)
$EnvelopedCMS.Encrypt($CMSRecipient) 
[Byte[]] $EncryptedBytes = $EnvelopedCMS.Encode() 

I'm not sure if the code above will work out of the box since my variable names may be different from yours.

The code above is only tested with Outlook2016.

Adil B
  • 14,635
  • 11
  • 60
  • 78
Jan
  • 36
  • 2
  • @abil That's fantastic. Thanks for that. Is there any chance you could convert so that it encrypts first and then signs (rather than sign and then encrypt). I've tried but I ended up with multiple layers of encrypt and signing and it didn't work. – gerard Nov 02 '17 at 16:53