Signing messages

Signatures validate the authenticity of the person who interacts with Nexmo.

You use a signature to:

  • Verify that a request originates from a trusted source
  • Ensure that the message has not been tampered with en-route
  • Defend against interception and later replay

A signature is the MD5 hash of:

  • The parameters - all the parameters in a request sorted in alphabetic order
  • A timestamp - a UNIX timestamp at UTC + 0 to protect against replay attacks
  • Your SECURITY_SECRET - the key supplied by Nexmo that you use to sign or validate requests

The signature has a leading '&'. All parameters in the hash input, apart from your SECURITY_SECRET are separated by '&'.

HMAC-SHA1/256/512 is also supported. Contact support@nexmo.com for more information.

Note: Using signatures is an optional improvement on using the standard api_secret. You use the SECURITY_SECRET instead of your api_secret in a signed request.

Note: If the text parameter contains & or = characters these should be first replaced with _ when used to construct the signature string. For example: You & Me = Love would become You _ Me _ Love.

The following example shows a signed request to the SMS API:

https://rest.nexmo.com/sms/xml?api_key=API_KEY&from=Nexmo&to=441632960960&type=text&text=Hello+from+Nexmo&status-report-req=false&timestamp=1461605396&sig=TwoMenWentToMowWentTOMowAMeadowT

The workflow for using signed messages is:

workflow_call_api_outbound
  1. Create a signed request to send an SMS.
  2. Check the response codes and ensure that you sent the request correctly.
  3. Your message is delivered to the handset. The user's handset returns a delivery receipt.
  4. If you requested signed delivery receipts and inbound messages validate the signature.

Setting up message signing

To setup message signing:

  1. Contact support@nexmo.com and request message signing. The options are:
    • Outbound messages can be signed.
    • Outbound messages must be signed.
    • Inbound messages and DLRs sent to your webhook endpoint are signed.
  2. Nexmo supplies you with the SECURITY_SECRET you use to encode and decode signatures.
    Note: this is not your api_secret.
  3. Implement the message signing workflow.

Implementing the message signing workflow

To sign your messages:

  1. Create a signed request:
    <?php
    
    $base_url = 'https://rest.nexmo.com/sms/json?';
    $security_secret = 'SECURITY_SECRET';
    
    //The timestamps used in the signature are in UTC + 0
    date_default_timezone_set('UTC');
    
    $params = [
          'api_key' =>  'API_KEY',
          'to' => '441632960960',
          'from' => '441632960061',
          'text' => 'Hello from Nexmo',
          'type' => 'text',
          'timestamp' => time() - date('Z')
        ];
    
    //sort your parameters
    ksort($params);
    
    //create base string
    $signing_url = '&' . urldecode(http_build_query($params)) . $security_secret;
    
    //Add your md5 hash of your parameters to your parameters
    $params['sig'] = md5($signing_url);
    
    //Create your request URL
    $url = $base_url . http_build_query($params);
    
    //Run your request
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    
    echo $response;
    
    import urllib
    import urllib2
    import time
    import md5
    import collections
    import json
    from datetime import datetime
    import calendar
    
    base_url = 'https://rest.nexmo.com/sms/json?'
    security_secret = 'SECURITY_SECRET'
    #The timestamps used in the signature are in UTC
    d = datetime.utcnow()
    
    params = {
        'api_key': 'API_KEY',
        'to': '441632960960',
        'from': '441632960961',
        'text': 'Hello from Nexmo',
        'type': 'text',
        'timestamp': calendar.timegm(d.utctimetuple())
    }
    # Sort your parameters
    sortedparams = collections.OrderedDict(sorted(params.items()))
    signing_url = '&' + urllib.unquote_plus(urllib.urlencode(sortedparams)) + security_secret
    
    #Add your md5 hash of your parameters to your parameters
    m = md5.new()
    m.update(signing_url)
    params['sig'] = m.hexdigest()
    
    #Create the request
    url = base_url + urllib.urlencode(params)
    request = urllib2.Request(url)
    request.add_header('Accept', 'application/json')
    
    #Make the request to Nexmo
    response = urllib2.urlopen(request)
    
    require 'net/http'
    require 'uri'
    require 'digest'
    require 'cgi'
    
    #Create your parameters
    base_url = 'https://rest.nexmo.com/sms/json'
    security_secret = 'SECURITY_SECRET'
    
    uri = URI.parse(base_url)
    params = {
        'api_key' => 'API_KEY',
        'to' => '441632960960',
        'from' => '441632960961',
        'text' => 'Hello from Nexmo',
        'type' => 'text',
        'timestamp' => Time.now.getutc.to_i
    }
    
    #Add your md5 hash of your sorted parameters to your parameters
    signing_url = '&' + CGI::unescape(URI.encode_www_form(params.sort)) + security_secret
    params['sig'] = Digest::MD5.hexdigest signing_url
    
    #Make the request to Nexmo
    response = Net::HTTP.post_form(uri, params)
    
    puts response.body
    
    
    var https = require('https');
    var crypto = require('crypto');
    
    var security_secret = 'SECURITY_SECRET';
    var security_method = 'sha256' // Possible values md5, sha1, sha256 or sha512
    
    var parameters = {
     api_key: 'API_KEY',
     to: '441632960960',
     from: '441632960961',
     text: 'Hello from Nexmo',
     type: 'text',
     timestamp: Math.floor(new Date() / 1000)
    };
    
    //Sort the parameters
    var param_array = new Array();
    for (key in parameters) {
        param_array.push(key + '=' + parameters[key]);
    }
    
    var sorted_params = param_array.sort();
    
    if (security_method == 'md5') {
        var signing_url = '&' + sorted_params.join('&') + security_secret ;
        var hash = crypto.createHash(security_method).update(signing_url).digest('hex');
    } else { 
        var signing_url = '&' + sorted_params.join('&');
        var hash = crypto.createHmac(security_method, security_secret).update(signing_url).digest('hex');
    }
    
    parameters['sig'] = hash ;
    var data = JSON.stringify(parameters );
    
    var options = {
     host: 'rest.nexmo.com',
     path: '/sms/json',
     port: 443,
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
       'Content-Length': Buffer.byteLength(data)
     }
    };
    
    var req = https.request(options);
    
    req.write(data);
    req.end();
    
    var responseData = '';
    req.on('response', function(res){
     res.on('data', function(chunk){
       responseData += chunk;
     });
     res.on('end', function(){
       console.log(JSON.parse(responseData));
     });
    });
    
  2. Check the response codes to ensure that you sent the request to Nexmo correctly:
    <?php
      //Decode the json object you retrieved when you ran the request.
      $decoded_response = json_decode($response, true);
    
      error_log('You sent ' . $decoded_response['message-count'] . ' messages.');
    
      foreach ( $decoded_response['messages'] as $message ) {
          if ($message['status'] == 0) {
              error_log("Success " . $message['message-id']);
          } else {
              error_log("Error {$message['status']} {$message['error-text']}");
          }
      }
    
    import json
    
    #Using the response object from the request
    
    if response.code == 200 :
        data = response.read()
        #Decode JSON response from UTF-8
        decoded_response = json.loads(data.decode('utf-8'))
        # Check if your messages are succesful
        messages = decoded_response["messages"]
        for message in messages:
            if message["status"] == "0":
                print "success"
    else :
        #Check the errors
        print "unexpected http {code} response from nexmo api". response.code
    
    require 'json'
    
    #Decode the json object from the response object you retrieved from the request.
    if response.kind_of? Net::HTTPOK
      decoded_response = JSON.parse(response.body )
    
      messagecount = decoded_response["message-count"]
    
      decoded_response["messages"].each do |message|
        if message["status"] == "0"
            p "message " + message["message-id"] + " sent successfully.\n"
          else
            p "message has error " + message["status"]  + " " + message["error-text"]
        end
      end
    else
      puts response.code + " error sending message"
    end
    
    //Decode the json object you retrieved when you ran the request.
    
    var decodedResponse = JSON.parse(responseData);
    
    console.log('You sent ' + decodedResponse['message-count'] + ' messages.\n');
    
    decodedResponse['messages'].forEach(function(message) {
        if (message['status'] === "0") {
          console.log('Success ' + decodedResponse['message-id']);
        }
        else {
          console.log('Error ' + decodedResponse['status']  + ' ' +  decodedResponse['error-text']);
        }
    });
    
    If you did not generate the signature correctly the status is 14, invalid signature.
  3. Your message is delivered to the handset. The user's handset returns a delivery receipt.
  4. If your delivery receipts and inbound messages are signed, validate the signature:
    <?php
    
    // work with get or post
    $request = array_merge($_GET, $_POST);
    $security_secret = 'SECURITY_SECRET';
    
    //If hash_equals is not supported by your version of PHP
    //Here is a timing attack safe string comparison
    if(!function_exists('hash_equals')) {
        function hash_equals($a, $b) {
            return substr_count($a ^ $b, "\0") * 2 === strlen($a . $b);
        }
    }
    
    //If the request has been signed
    if(isset($request['sig'])){
      //The timestamps used in the signature are in UTC + 0
      //Create a UTC timestamp for now and compare
      $now = time() - date('Z');
      $message_timestamp = strtotime($request['message-timestamp']);
      $difference = abs ($now - $message_timestamp);
    
      //Message cannot be more than 5 minutes old
      $max_delta = 5 * 60;
      if ($difference > $max_delta)
          error_log("Timestamp difference greater than 5 minutes");
      else {
        //Store the signature locally and remove from the params
        $message_signature = $request['sig'];
        unset($request['sig']);
        // Sort the parameters so they are in alphabetic order
        ksort($request);
        // Generate a signature from the parameters plus your security secret
        $generated_signature = md5('&' .urldecode(http_build_query($request)) .$security_secret);
        // A timing attack safe string comparison to validate the signatures
        if (hash_equals($message_signature, $generated_signature))
            error_log("Message was sent by Nexmo");
        else
            error_log("Alert: message not sent by Nexmo!");
      }
    }
    
    #To run this code, replace the MyHandler in
    #https://wiki.python.org/moin/BaseHttpServer With the following code,
    import urllib
    import time
    import BaseHTTPServer
    import re
    import json
    from urlparse import urlparse, parse_qs
    from datetime import datetime
    import datetime
    import collections
    import md5
    import calendar
    
    security_secret = 'SECURITY_SECRET'
    
    class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
        def do_GET(s):
            """Tell Nexmo that you have recieved the GET request."""
            s.send_response(200)
            s.send_header("Content-type", "text/html")
            s.end_headers()
            """Parse parameters in the GET request"""
            parsed_path = urlparse(s.path)
            try:
                    callback = dict(
                    [p.split('=') for p in parsed_path[4].split('&')])
            except:
                    callback = {}
    
            #Check if the callback is signed
            if 'sig' in callback:
                #Message cannot be more than 5 minutes old
                #The timestamps used in the signature are in UTC
                #Create a UTC timestamp for now and compare
                d = datetime.utcnow()
                now = calendar.timegm(d.utctimetuple())
                message_timestamp = int( callback['timestamp'])
                max_delta = 5 * 60
                difference = abs(now - message_timestamp)
                if (difference >  max_delta ):
                    print("Timestamp difference greater than 5 minutes")
                else:
                    #Remove the signature from the request parameters
                    message_signature = callback['sig']
                    del callback['sig']
                    # Sort the parameters into alphabetic order
                    sortedparams = collections.OrderedDict(sorted(callback.items()))
                    # Remove the encoding from the timestamp, then put it back in the params
                    tempstamp = sortedparams['message-timestamp']
                    tempstamp = urllib.unquote_plus(tempstamp).decode('utf8')
                    sortedparams['message-timestamp'] = tempstamp
                    #Generate a signature from the parameters and your security secret
                    encoded_params = urllib.urlencode(sortedparams)
                    signing_url = '&' + urllib.unquote_plus(encoded_params).decode('utf8')
                    signing_url += security_secret
                    m = md5.new()
                    m.update(signing_url)
                    generated_signature = m.hexdigest()
                    # Validate that the signatures match
                    if ( constant_time_compare (message_signature, generated_signature)):
                        print("Message was sent by Nexmo")
                    else :
                        print("Alert: message not sent by Nexmo!")
    
            else:
                print "This callback has not been signed"
    
    # A timing attack safe string comparison
    def constant_time_compare(val1, val2):
        """
        Returns True if the two strings are equal.
        This function executes in constant time only when the two strings have the same length
        """
        if len(val1) != len(val2):
            return False
        result = 0
        for x, y in zip(val1, val2):
            result |= ord(x) ^ ord(y)
        return result == 0
    
    require 'socket'
    require "net/http"
    require "uri"
    require 'digest'
    require 'cgi'
    
    def handle_delivery_receipt(request_line, security_secret)
    #Parse the parameters and check if the message was delivered
    
      params = URI::decode_www_form(request_line).to_h
      if ! params["sig"].nil?
        now = Time.now.getutc.to_i
        message_timestamp = params['timestamp'].to_i
        max_delta = 5 * 60
        difference =  now - message_timestamp
        if ( difference.abs >  max_delta )
            p("Timestamp difference greater than 5 minutes")
        else
          #Remove the signature from the request parameters
          message_signature = params['sig']
          params['msisdn'] = params['/?msisdn']
          params.delete('sig')
          params.delete('/?msisdn')
    
          # Create the signing url using the sorted parameters and your SECURITY_SECRET
          signing_url = '&' + CGI::unescape(URI.encode_www_form(params.sort)) + security_secret
          #Add your md5 hash of your parameters to your parameters
          generated_signature = Digest::MD5.hexdigest(signing_url)
    
          if (secure_compare( message_signature, generated_signature))
            p("Message was sent by Nexmo");
          else
            print("Alert: message not sent by Nexmo!")
          end
        end
      end
    end
    
    # A timing attack safe string comparison
    def secure_compare(a, b)
         return false if a.empty? || b.empty? || a.bytesize != b.bytesize
         l = a.unpack "C#{a.bytesize}"
    
         res = 0
         b.each_byte { |byte| res |= byte ^ l.shift }
         res == 0
       end
    
    security_secret = 'SECURITY_SECRET'
    # Initialize a TCPServer
    server = TCPServer.new('', 9999)
    
    # Wait for connections
    loop do
      # Wait until a client connects
      socket = server.accept
    
      method, path = socket.gets.split
      #Check the signature
      handle_delivery_receipt(path, security_secret)
    
      # Return the 200 so Nexmo does not send the DLR to you repeatedly
      resp = "Thank you"
      headers = ["HTTP/1.1 200 OK",
                 "Content-Type: text/html; charset=iso-8859-1",
                 "Content-Length: #{resp.length}\r\n\r\n"].join("\r\n")
      socket.puts headers
      socket.puts resp
      # Close the socket, terminating the connection
      socket.close
    end
    
    var http = require('http')
    var url = require('url')
    var crypto = require('crypto');
    
    var security_secret = 'SECURITY_SECRET';
    // create the http server
    http.createServer(function (request, response) {
    
      if(request.method=='POST') {
          //Do something for post request
          console.log("post message");
      }
      else if(request.method=='GET') {
          //Turn the query string onto an object
          var url_parts = url.parse(request.url,true).query;
          if (url_parts.hasOwnProperty('sig')){
    
            //Compare the local time with the timestamp
            var now = Math.floor(new Date() / 1000);
            var message_timestamp = url_parts.timestamp;
    
            //Message cannot be more than 5 minutes old
            var max_delta = 5 * 60;
            difference = Math.abs( now - message_timestamp );
            if (difference > max_delta)
                console.log("Timestamp difference greater than 5 minutes");
            else {
                //Sort the parameters
                message_signature = url_parts.sig;
                //Remove the signature from the request parameters
                delete url_parts.sig;
    
                //Create the signing url using the sorted parameters and your SECURITY_SECRET
                var param_array = new Array();
                for (key in url_parts) {
                    param_array.push(key + '=' + unescape(url_parts[key]));
                }
                var sorted_params = param_array.sort();
                var signing_url = '&' + sorted_params.join('&') + security_secret ;
    
                //Add your md5 hash of your parameters to your parameters
                var generated_signature = crypto.createHash('md5').update(signing_url).digest('hex');
                //A timing attack safe string comparison to validate hash
                var valid = 0;
                for (var i = 0; i < generated_signature.length; ++i) {
                    valid |= (generated_signature.charCodeAt(i) ^ message_signature.charCodeAt(i));
                }
    
                if (valid == 0)
                  console.log("Message was sent by Nexmo");
                else
                  console.log("Alert: message not sent by Nexmo!");
            }
    
          }
      }
      //Send the 200 ok to Nexmo so you don't get sent the DLR again.
      response.writeHead(200, {"Content-Type": "text/html"});
      response.write("hello iain");
      response.end();
    
    }).listen(80);
    
Previous   Next