Signing requests
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
SIGNATURE_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 SIGNATURE_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 theSIGNATURE_SECRET
instead of your api_secret in a signed request.
The following example shows a signed request to the SMS API:
https://rest.nexmo.com/sms/xml?api_key=API_KEY&from=Nexmo&to=447700900000&type=text&text=Hello+from+Nexmo&status-report-req=false×tamp=1461605396&sig=SIGNATURE
The workflow for using signed messages is:
- Create a signed request to send an SMS.
- Check the response codes and ensure that you sent the request correctly.
- Your message is delivered to the handset. The user's handset returns a delivery receipt.
- If you requested signed delivery receipts and inbound messages validate the signature.
Setting up message signing
To setup message signing:
- 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.
-
Nexmo supplies you with the
SIGNATURE_SECRET
you use to encode and decode signatures.Note: this is not your
api_secret
. Implement the message signing workflow.
Implementing the message signing workflow
To sign your messages:
-
Create a signed request:
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)); }); });
<?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
-
Check the response codes to ensure that you sent the request to Nexmo correctly:
//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']); } });
<?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 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
If you did not generate the signature correctly the status is
14, invalid signature
Your message is delivered to the handset. The user's handset returns a delivery receipt.
-
If your delivery receipts and inbound messages are signed, validate the signature:
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);
<?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