Skip to content

Instantly share code, notes, and snippets.

@damusix
Last active October 24, 2024 06:12
Show Gist options
  • Save damusix/c12400ee0ccb7e56351619ae2b19a303 to your computer and use it in GitHub Desktop.
Save damusix/c12400ee0ccb7e56351619ae2b19a303 to your computer and use it in GitHub Desktop.
Convert AWS IAM credentials to AWS SMTP credentials

Convert AWS IAM credentials to AWS SMTP credentials

If you do, or want to, use AWS to deploy your apps, you will end up using AWS SES via SMTP when you're launching an app that sends out emails of any kind (user registrations, email notifications, etc). For example, I have used this configuration on various Ruby on Rails apps, however, it is just basic SMTP configurations and crosses over to any framework that supports SMTP sendmail.

There are two ways to go about this:

Luckily, you found this MD file and the NOT SO EASY WAY is suddenly copy-pasta... sudo yum....

Assuming you've already set up your SES Policy on your IAM User:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect":"Allow",
          "Action":["ses:SendEmail", "ses:SendRawEmail"],
          "Resource":"*"
        }
      ]
    }

Go ahead and drop this into an bash session, or somewhere in your app, and pass in your IAM user's secret key to generate your SMTP password :)

Ruby

Thanks @talreg and @cristim and @jschroed91

require 'openssl'
require 'base64'

def aws_iam_smtp_password_generator(key, region)
      # The values of the following variables should always stay the same.
      date = "11111111"
      service = "ses"
      terminal = "aws4_request"
      message = "SendRawEmail"
      version_in_bytes = "\x04"

      k_date = OpenSSL::HMAC.digest('sha256', "AWS4" + key, date)
      k_region = OpenSSL::HMAC.digest('sha256', k_date, region)
      k_service = OpenSSL::HMAC.digest('sha256', k_region, service)
      k_terminal = OpenSSL::HMAC.digest('sha256', k_service, terminal)
      k_message = OpenSSL::HMAC.digest('sha256', k_terminal, message)
      signature_and_version = version_in_bytes + k_message
      smtp_password = Base64.encode64(signature_and_version)

      smtp_password.to_s.strip
end

# print aws_iam_smtp_password_generator(ENV['AWS_SECRET_ACCESS_KEY'], "us-east-1")

PHP

    <?php

      function aws_iam_smtp_password_generator($secret) {
        $message = "SendRawEmail";
        $versionInBytes = chr(2);
        $signatureInBytes = hash_hmac('sha256', $message, $secret, true);
        $signatureAndVer = $versionInBytes.$signatureInBytes;
        $smtpPassword = base64_encode($signatureAndVer);

        return $smtpPassword;
      }

    ?>

Python

Thanks to @avdhoot and @techsolx

# v2
import base64
import hmac
import hashlib
import sys

def hash_smtp_pass_from_secret_key(key):
    message = "SendRawEmail"
    version = '\x02'
    h = hmac.new(key, message, digestmod=hashlib.sha256)
    return base64.b64encode("{0}{1}".format(version, h.digest()))

if __name__ == "__main__":
    print hash_smtp_pass_from_secret_key(sys.argv[1])


#####################


# v3
import argparse
import base64
import hashlib
import hmac


def hash_iam_secret(sakey, version):
    key_bytes = str.encode(sakey)
    message_bytes = str.encode('SendRawEmail')
    version_bytes = str.encode(version)
    dig = hmac.new(key_bytes, message_bytes, digestmod=hashlib.sha256)
    return base64.b64encode(version_bytes+dig.digest()).decode()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('iam_secret_access_key',
                        type=str,
                        help='The AWS IAM secret access key')
    parser.add_argument('version',
                        type=str,
                        nargs='?',
                        default='\0x2',
                        help='Optional version number, default is 2')
    args = parser.parse_args()

    if len(args.iam_secret_access_key) != 40:
        print('AWS secret access keys should be 40 characters.')
    else:
        dig = hash_iam_secret(args.iam_secret_access_key,
                              args.version)

    print(dig)


if __name__ == '__main__':
    main()

Go

Thanks @talreg and @anieri

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
)

const (
	awsDate     string = "11111111"
	awsService  string = "ses"
	awsMessage  string = "SendRawEmail"
	awsTerminal string = "aws4_request"
	awsVersion  byte   = 0x04
)

func DeriveSMTPCredential(region, secretKey string) string {
	signature := sign([]byte("AWS4"+secretKey), []byte(awsDate))
	signature = sign(signature, []byte(region))
	signature = sign(signature, []byte(awsService))
	signature = sign(signature, []byte(awsTerminal))
	signature = sign(signature, []byte(awsMessage))

	infoWithSignature := make([]byte, 1+len(signature))
	infoWithSignature[0] = awsVersion
	copy(infoWithSignature[1:], signature)

	return base64.StdEncoding.EncodeToString(infoWithSignature)
}

func sign(key, msg []byte) []byte {
	h := hmac.New(sha256.New, key)
	h.Write(msg)

	return h.Sum(nil)
}

PowerShell

https://gist.github.com/jacqueskang/96c444ee01e6a4b37300aa49e8097513

$key = "${SecretAccessKey}";
$region = "${AWS::Region}";

$date = "11111111";
$service = "ses";
$terminal = "aws4_request";
$message = "SendRawEmail";
$versionInBytes = 0x04;

function HmacSha256($text, $key2) {
    $hmacsha = New-Object System.Security.Cryptography.HMACSHA256
    $hmacsha.key = $key2;
    $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($text));
}

$signature = [Text.Encoding]::UTF8.GetBytes("AWS4" + $key)
$signature = HmacSha256 "$date" $signature;
$signature = HmacSha256 "$region" $signature;
$signature = HmacSha256 "$service" $signature;
$signature = HmacSha256 "$terminal" $signature;
$signature = HmacSha256 "$message" $signature;
$signatureAndVersion = [System.Byte[]]::CreateInstance([System.Byte], $signature.Length + 1);
$signatureAndVersion[0] = $versionInBytes;
$signature.CopyTo($signatureAndVersion, 1);
$smtpPassword = [Convert]::ToBase64String($signatureAndVersion);

Write-Host $smtpPassword;

Erlang

#!/usr/bin/env escript
%% -*- erlang -*-
-define(DATE    , <<"11111111">>    ).
-define(SERVICE , <<"ses">>         ).
-define(MESSAGE , <<"SendRawEmail">>).
-define(TERMINAL, <<"aws4_request">>).
-define(VERSION , 4                ).

main([Key,Region]) ->
    KeyBinary           = list_to_binary(Key),
    RegionBinary        = list_to_binary(Region),
    Sig1                = sign(<<"AWS4", KeyBinary/binary>>,?DATE),
    Sig2                = sign(Sig1,RegionBinary),
    Sig3                = sign(Sig2, ?SERVICE),
    Sig4                = sign(Sig3, ?TERMINAL),
    Sig5                = sign(Sig4, ?MESSAGE),
    SignatureAndVersion = << ?VERSION,Sig5/binary>>,
    HB                  = base64:encode(SignatureAndVersion),
    io:format("~s\n",[HB]);

main(_) ->
    usage().

sign(Key,Msg) ->
  crypto:mac(hmac,sha256,Key,Msg).

usage() ->
    io:format("usage: ~p secret_access_key region\n",[escript:script_name()]),
    halt(1).

Java

I'm not a Java programmer, yet, but AWS's documentation [http://docs.aws.amazon.com/ses/latest/DeveloperGuide/smtp-credentials.html#smtp-credentials-convert] has a snippet you can use

I spent way too much time figuring this stuff out. I hope this helps!!!

HAPPY SES-ing!

@matthias-schoeneich
Copy link

erlang escript

#!/usr/bin/env escript
%% -*- erlang -*-
-define(DATE    , <<"11111111">>    ).
-define(SERVICE , <<"ses">>         ).
-define(MESSAGE , <<"SendRawEmail">>).
-define(TERMINAL, <<"aws4_request">>).
-define(VERSION , 4                ).

main([Key,Region]) ->
    KeyBinary           = list_to_binary(Key),
    RegionBinary        = list_to_binary(Region),
    Sig1                = sign(<<"AWS4", KeyBinary/binary>>,?DATE),
    Sig2                = sign(Sig1,RegionBinary),
    Sig3                = sign(Sig2, ?SERVICE),
    Sig4                = sign(Sig3, ?TERMINAL),
    Sig5                = sign(Sig4, ?MESSAGE),
    SignatureAndVersion = << ?VERSION,Sig5/binary>>,
    HB                  = base64:encode(SignatureAndVersion),
    io:format("~s\n",[HB]);

main(_) ->
    usage().

sign(Key,Msg) ->
  crypto:mac(hmac,sha256,Key,Msg).

usage() ->
    io:format("usage: ~p secret_access_key region\n",[escript:script_name()]),
    halt(1).

@jschroed91
Copy link

jschroed91 commented Sep 23, 2020

If anyone needs an updated ruby script for version 4:

require 'openssl'
require 'base64'

def aws_iam_smtp_password_generator(key, region)
      # The values of the following variables should always stay the same.
      date = "11111111"
      service = "ses"
      terminal = "aws4_request"
      message = "SendRawEmail"
      version_in_bytes = "\x04"

      k_date = OpenSSL::HMAC.digest('sha256', "AWS4" + key, date)
      k_region = OpenSSL::HMAC.digest('sha256', k_date, region)
      k_service = OpenSSL::HMAC.digest('sha256', k_region, service)
      k_terminal = OpenSSL::HMAC.digest('sha256', k_service, terminal)
      k_message = OpenSSL::HMAC.digest('sha256', k_terminal, message)
      signature_and_version = version_in_bytes + k_message
      smtp_password = Base64.encode64(signature_and_version)

      smtp_password.to_s.strip
end

# print aws_iam_smtp_password_generator(ENV['AWS_SECRET_ACCESS_KEY'], "us-east-1")

@ablionit
Copy link

I am getting a warning from AWS that this signature is version 2, which will be deprecated on March 2021, so we need to use signature 4.

can you update the PHP function for the same so that we can use it.

@mcdonnez
Copy link

Here is the updated php version of this

function aws_iam_smtp_password_generator($key, $region) {
	$date = "11111111";
	$service = "ses";
	$terminal = "aws4_request";
	$message = "SendRawEmail";
	$versionInBytes = chr(4);

	// HmacSha256
	$kDate = hash_hmac('sha256', $date, "AWS4" . $key, true);
	$kRegion = hash_hmac('sha256', $region, $kDate, true);
	$kService = hash_hmac('sha256', $service, $kRegion, true);
	$kTerminal = hash_hmac('sha256', $terminal, $kService, true);
	$kMessage = hash_hmac('sha256', $message, $kTerminal, true);
	$signatureAndVersion = $versionInBytes . $kMessage;
	$smtpPassword = base64_encode($signatureAndVersion);
	return $smtpPassword;
}

@galoggob
Copy link

Java

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

/**

  • @see The following pseudocode shows an algorithm that converts an AWS secret access key to an Amazon SES SMTP password.
  •  https://docs.aws.amazon.com/ses/latest/DeveloperGuide/smtp-credentials.html#smtp-credentials-console
    

*/
public class SesSmtpCredentialGenerator {

public String getSmtpPasswordV4(String key){
	// The values of the following variables should always stay the same.
	final String message = "SendRawEmail";
	final String date = "11111111";
	final String service = "ses";
	final String terminal = "aws4_request";
	final String region = "eu-west-1";
	final byte VERSION4 = 0x04; // Version number. Do not modify.
	
	try {
	    byte[] kDate = sign(date, ("AWS4" +key).getBytes());
	    byte[] kRegion = sign(region, kDate);
	    byte[] kService = sign(service, kRegion);
	    byte[] kTerminal = sign(terminal, kService);
	    byte[] kMessage = sign(message, kTerminal);
	    
	    // Snippet code-> signatureAndVersion = Concatenate(version, kMessage);
	    byte[] rawSignatureWithVersion = new byte[kMessage.length + 1];
	    byte[] versionArray = {VERSION4};
	    System.arraycopy(versionArray, 0, rawSignatureWithVersion, 0, 1);
	    System.arraycopy(kMessage, 0, rawSignatureWithVersion, 1, kMessage.length);

	    // Snippet code-> smtpPassword = Base64(signatureAndVersion);
	    String smtpPassword = DatatypeConverter.printBase64Binary(rawSignatureWithVersion);
	    System.out.println(smtpPassword);
	      
		return smtpPassword;
	} catch (Exception ex) {
		System.out.println("Error generating SMTP password: " + ex.getMessage());
	}
	return null;
}

private static byte[] sign(String msg, final byte[] key) throws NoSuchAlgorithmException, InvalidKeyException {
	SecretKeySpec secretKey = new SecretKeySpec(key, "HmacSHA256");
	// Get an HMAC-SHA256 Mac instance and initialize it with the AWS secret access key.
	Mac mac = Mac.getInstance("HmacSHA256");
	mac.init(secretKey);
	return mac.doFinal(msg.getBytes());
}

}

@azumakuniyuki
Copy link

Perl implementation is here: https://gist.github.com/azumakuniyuki/b3baec18e9d06e9063c62029a2991303

#!/usr/bin/env perl
use strict;
use warnings;
use MIME::Base64;
use Digest::SHA;

my $secret = shift or die('The 1st argument should be an access secret key');
my $region = shift or die('The 2nd argument should be a valid region name');
my $params = {
    'date'      => '11111111',
    'service'   => 'ses',
    'message'   => 'SendRawEmail',
    'terminal'  => 'aws4_request',
    'version'   => "\x04",
};

my $digest = makedigest('AWS4'.$secret, $params->{'date'});
   $digest = makedigest($digest, $region);
   $digest = makedigest($digest, $params->{'service'});
   $digest = makedigest($digest, $params->{'terminal'});
   $digest = makedigest($digest, $params->{'message'});
printf("%s\n", MIME::Base64::encode_base64(sprintf("%s%s", $params->{'version'}, $digest), ''));

sub makedigest {
    my $key = shift // return undef;
    my $msg = shift // return undef;
    return Digest::SHA::hmac_sha256($msg, $key);
}

@jesusgoku
Copy link

NodeJS's implementation, without dependencies (only core modules):

https://gist.github.com/jesusgoku/533ba67ea218608c5a74a6d341d75189

#!/usr/bin/env node

/**
 * Obtaining Amazon SES SMTP credentials by converting existing AWS credentials
 *
 * Script based on:
 * https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html
 */

const crypto = require('crypto');

const SMTP_REGIONS = [
  'us-east-2', // US East (Ohio)
  'us-east-1', // US East (N. Virginia)
  'us-west-2', // US West (Oregon)
  'ap-south-1', // Asia Pacific (Mumbai)
  'ap-northeast-2', // Asia Pacific (Seoul)
  'ap-southeast-1', // Asia Pacific (Singapore)
  'ap-southeast-2', // Asia Pacific (Sydney)
  'ap-northeast-1', // Asia Pacific (Tokyo)
  'ca-central-1', // Canada (Central)
  'eu-central-1', // Europe (Frankfurt)
  'eu-west-1', // Europe (Ireland)
  'eu-west-2', // Europe (London)
  'sa-east-1', // South America (Sao Paulo)
  'us-gov-west-1', // AWS GovCloud (US)
];

// These values are required to calculate the signature. Do not change them.
const DATE = '11111111';
const SERVICE = 'ses';
const MESSAGE = 'SendRawEmail';
const TERMINAL = 'aws4_request';
const VERSION = [0x04];

function sign(key, msg) {
  return crypto.createHmac('sha256', key).update(msg).digest();
}

function calculate_key(secret_access_key, region) {
  if (!SMTP_REGIONS.includes(region)) {
    throw new Error(`The ${region} Region doesn't have an SMTP endpoint`);
  }

  let signature;

  signature = sign(`AWS4${secret_access_key}`, DATE);
  signature = sign(signature, region);
  signature = sign(signature, SERVICE);
  signature = sign(signature, TERMINAL);
  signature = sign(signature, MESSAGE);

  const signature_and_version = Buffer.concat([
    Buffer.from(VERSION),
    signature,
  ]);

  const smtp_password = Buffer.from(signature_and_version).toString('base64');

  return smtp_password;
}

function main() {
  const [secret, region] = process.argv.slice(2);

  console.log(calculate_key(secret, region));
}

if (require.main === module) {
  main();
}

@gpfeifer
Copy link

Rust

A quick&dirty implementation in Rust.

Cargo.toml

[package]
name = "iam2smpt"
version = "0.1.0"
edition = "2021"

[dependencies]
base64 = "0.21.2"
clap = { version = "4.3.5", features = ["derive"] }
hmac-sha256 = "1.1.7"

main.rs

use clap::{Parser, command};
use hmac_sha256::HMAC;
use base64::{Engine as _, engine::{general_purpose}};

const DATE: &str = "11111111";
const SERVICE: &str = "ses";
const MESSAGE: &str = "SendRawEmail";
const TERMINAL: &str = "aws4_request";
const VERSION: u8 = 0x04;

#[test]
fn test_iam2smtp() {
    let key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
    let region = "us-east-1";
    let sig = smtp_credential(key, region);
    assert_eq!(sig, "BLBM/9hSUELfq8Gw+rU1YcBjkOxGbhT2XG763xVLGWL9");
}

#[inline]
fn sign(key: &[u8], msg: &str) -> [u8; 32] {
    HMAC::mac(msg,key)
}

fn smtp_credential(secret_key: &str, region: &str) -> String {
    let key = format!("AWS4{}",secret_key);
    let mut signature = sign (key.as_bytes(), DATE);
    signature = sign(&signature, region);
    signature = sign(&signature, SERVICE);
    signature = sign(&signature, TERMINAL);
    signature = sign(&signature, MESSAGE);
    let mut result = vec![VERSION];
    result.extend_from_slice(&signature);
    general_purpose::STANDARD.encode(result)
}

#[derive(Parser)]
#[command(author, version, about)]
struct Args {
    /// The Secret Access Key to convert.
    secret: String,
    /// The AWS Region where the SMTP password will be used.   
    region: String,
}

fn main() {
    let args = Args::parse();
    println!("{}", smtp_credential(&args.secret, &args.region));
}

@sonnykt
Copy link

sonnykt commented Feb 13, 2024

The PHP conversion no longer works as AWS SDK is on v4 now.

PHP

Updated conversion for V4.

function aws_iam_smtp_password_generator_v4(string $secret, string $region) : string {
        $date = "11111111";
        $service = "ses";
        $terminal = "aws4_request";
        $message = "SendRawEmail";
        $version = 0x04;

        $signature = hash_hmac('sha256', $date, "AWS4" . $secret, true);
        $signature = hash_hmac('sha256', $region, $signature, true);
        $signature = hash_hmac('sha256', $service, $signature, true);
        $signature = hash_hmac('sha256', $terminal, $signature, true);
        $signature = hash_hmac('sha256', $message, $signature, true);
        $signatureAndVersion = pack('c', $version) . $signature;

        return  base64_encode($signatureAndVersion);
}

Alternatively, the AWS SDK could be used: SesClient::generateSmtpPasswordV4().

@samuelsons
Copy link

samuelsons commented May 12, 2024

Thank you for the great assitance.

I was trying to test my SES-dashboard generated SMTP-credentials via powershell command line using the template below, but I keep getting this error, any ideas ?

I am using a production ready account, verified domain & from-email address,

Error
Exception calling "Send" with "4" argument(s): "The SMTP server requires a secure connection or the client was not authenticated. The server response was: Authentication required"
At line:13 char:1
$SMTPClient.Send($EmailFrom, $EmailTo, $Subject, $Body)

    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : SmtpException`
    
**Powershell Script below:**
>> <# Source:
>>    https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-smtp-client-command-line.html
>> #>
>> $EmailFrom = "contact@r***********.com"
>> $EmailTo = "***********"
>> $Subject = "Hello from Amazon SES SMTP Test for Windows"
>> $Body = "This message was sent using the Amazon SES SMTP interface. There is no more content in this test email."
>> $SMTPServer = "email-smtp.eu-west-1.amazonaws.com"
>> $SMTPClient = New-Object Net.Mail.SmtpClient($SmtpServer, 587)
>> $SMTPClient.EnableSsl = $true
>> $SMTPClient.Credentials = New-Object System.Net.NetworkCredential("AKIA4************", "**********************************************");
>> $SMTPClient.Send($EmailFrom, $EmailTo, $Subject, $Body)
>> Remove-Variable -Name SMTPClient

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment