Signature validation-V3

All requests from Plivo’s Voice platform to your application server contain X-Plivo-Signature-V3, X-Plivo-Signature-Ma-V3, and X-Plivo-Signature-V3-Nonce HTTP headers. To verify that the request to your server originated from Plivo and that it was not altered en route, you may generate your own request signatures and compare them with the X-Plivo-Signature-V3 or X-Plivo-Signature-Ma-V3 HTTP headers passed by Plivo.

Signature generation logic

Plivo signs all HTTP requests from its servers to your application server, and assembles the request to your application server by concatenating the final request URL (the full URL with the scheme, port, and query string) and any POST parameters.

  • If your request is a POST, Plivo will take all the POST parameters, sort them alphabetically by name (using Unix-style case-sensitive sorting), and append the parameter name and value pairs to the end of the URL.
  • If the request is a GET, the final request URL will include all of Plivo’s request parameters appended to the query string of your original URL.

Example:

If your application URL is https://example.com/abcd?foo=bar and the list of Plivo POST parameters is:

- Digits:1234
- To:+15555555555
- From:+15551111111
- CallUUID:4vbcpem8-0u46-x1ha-9af1-438vc92bf374

Then the complete assembled request string would be:

- https://example.com/abcd?foo=bar.CallUuid4vbcpem8-0u46-x1ha-9af1-438vc92bf374Digits1234From+15551111111To+15555555555

Concatenating with nonce value

The assembled request string from this step is then appended with a randomly generated nonce string that’s unique for every request. This string is passed separately in the HTTP header X-Plivo-Signature-V3-Nonce as part of the HTTP request to your application URL.

Example:

If the randomly generated nonce value is

kjsdhfsd87sd7yisud2

then this is how the request string would look after appending the nonce to the previously assembled request string.

- https://example.com/abcd?foo=bar.CallUuid4vbcpem8-0u46-x1ha-9af1-438vc92bf374Caller+15551111111Digits1234From+15551111111To+15555555555.kjsdhfsd87sd7yisud2

Since nonce values are unique, they can help protect your application server from replay attacks.

Signing with Auth Token

The output of this step is then signed using HMAC-SHA256 and your Plivo Auth Token as the key. The resulting signed hashes are Base64-encoded and passed in HTTP headers with the request.

X-Plivo-Signature-V3 is generated using the Auth Token of the account or subaccount associated with the request entity.

For example, if the request is for an incoming call to a Plivo phone number mapped to one of your subaccounts, then that subaccount’s Auth Token will be used when generating X-Plivo-Signature-V3.

X-Plivo-Signature-Ma-V3, on the other hand, is always generated using the Auth Token of your main Plivo account.

Note: If you have more than one active Auth Token for the associated account or subaccount, Plivo will generate a signature using each of the active Auth Tokens and pass a comma-separated list of the resulting signatures to the application server (for example, X-Plivo-Signature-Ma-V3: <Signature signed with Token 1>,<Signature signed with Token 2>).

Validating requests on the application server

Plivo Server SDKs include helper functions to validate incoming requests with X-Plivo-Signature HTTP headers.

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from flask import Flask, request, make_response
import plivo
from plivo import plivoxml

app = Flask(__name__)


@app.route("/speak/", methods=["GET", "POST"])
def validate_signature():
   auth_token = "<auth_token>"
   signature = request.headers.get("X-Plivo-Signature-V3")
   nonce = request.headers.get("X-Plivo-Signature-V3-Nonce")
   webhook_url = request.url
   http_method = request.method

   if http_method == "GET":
       valid_signature = plivo.utils.validate_v3_signature(
           http_method, webhook_url, nonce, auth_token, signature
       )
   elif http_method == "POST":
       params = request.form.to_dict()
       valid_signature = plivo.utils.validate_v3_signature(
           http_method, webhook_url, nonce, auth_token, signature, params
       )
   else:
       return "Method not allowed", 405

   print(f"Your webhook authentication is: {valid_signature}")

   # Return an XML answer to Plivo if the signature is valid

   if valid_signature == True:
       xml = plivoxml.ResponseElement()
       speak_params = {"loop": "3"}
       xml.add(plivoxml.SpeakElement("Hello, from Plivo", **speak_params))
       response = make_response(xml.to_string())
       response.headers["Content-type"] = "text/xml"
       print("Send XML to Plivo server")
       print(xml.to_string())
       return response, 200
   else:
       return "Bad request", 400


if __name__ == "__main__":
   app.run(host="0.0.0.0", debug=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
require 'sinatra'
require 'rubygems'
require 'plivo'
include Plivo
include Plivo::XML

get '/speak/' do
	auth_token = "<auth_token>"
	signature = headers.fetch("X-Plivo-Signature-V3", "signature")
	nonce = headers.fetch("X-Plivo-Signature-V3-Nonce", "12345")
	url = request.url
	method = "GET"
	output = Plivo::Utils.valid_signatureV3?(url, nonce, signature, auth_token, method)
	puts output

	response = Response.new
	response.addSpeak("Hello, Welcome to Plivo")
	xml = PlivoXML.new(response)
	content_type 'text/xml'
	return xml.to_s
end

post '/speak/' do
	auth_token = "<auth_token>"
	signature = headers.fetch("X-Plivo-Signature-V3", "signature")
	nonce = headers.fetch("X-Plivo-Signature-V3-Nonce", "12345")
	url = request.url
	method = "POST"
	output = Plivo::Utils.valid_signatureV3?(url, nonce, signature, auth_token, method, params)
	puts output

	response = Response.new
	response.addSpeak("Hello, Welcome to Plivo")
	xml = PlivoXML.new(response)
	content_type 'text/xml'
	return xml.to_s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
let express = require('express');
let app = express();
app.set('port', (process.env.PORT || 5000));
app.use(express.static(__dirname + '/public'));

app.all('/speak/', function (request, response) {
    let headers = request.headers;
    console.log(headers);
    let signature = request["X-Plivo-Signature-V3"];
    let nonce = request["X-Plivo-Signature-V3-Nonce"];
    if (!signature) {
        signature = "signature";
    }
    if (!nonce) {
        nonce = "12345";
    }
    let url = request.url;
    let auth_token = "<auth_token>";
    console.log(signature, nonce);
    let method = request.method;
    let validate;
    if (method == "GET") {
        validate = plivo.validateV3Signature(method, url, nonce, auth_token, signature);
    } else {
        let params = request.body;
        validate = plivo.validateV3Signature(method, url, nonce, auth_token, signature, params);
    }
    console.log(validate);

    let r = plivo.Response();
    r.addSpeak("Hello from Plivo");
    console.log(r.toXML());

    response.set({
        'Content-Type': 'text/xml'
    });
    response.end(r.toXML());
});

app.listen(app.get('port'), function () {
    console.log('Node app is running on port', app.get('port'));
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
require 'vendor/autoload.php';
use Plivo\Exceptions\PlivoValidationException;
use Plivo\Util\v3SignatureValidation;
use Plivo\XML\Response;

if (preg_match('/speak/', $_SERVER["REQUEST_URI"]))
{
    $auth_token = "<auth_token>";
    $signature = @$_SERVER["HTTP_X_PLIVO_SIGNATURE_V3"] ? : 'signature';
    $nonce = @$_SERVER["HTTP_X_PLIVO_SIGNATURE_V3_NONCE"] ? : 'nonce';
    $url = $_SERVER['HTTP_REFERER'];
    $method = $_SERVER['REQUEST_METHOD'];
    $SVUtil = new v3SignatureValidation();
    if ($method == "GET")
    {
        try
        {
            $valid = $SVUtil->validateV3Signature($method, $url, $nonce, $auth_token, $signature);
        }
        catch(PlivoValidationException $e)
        {
            echo ("error");
        }
    }
    else
    {
        $body = file_get_contents("php://input", true);
        parse_str($body, $get_array);
        try
        {
            $valid = $SVUtil->validateV3Signature($method, $url, $nonce, $auth_token, $signature, $get_array);
        }
        catch(PlivoValidationException $e)
        {
            echo ("error");
        }
    }
    error_log(print_r($valid, true));
    $body = 'Hi, Calling from Plivo';
    $attributes = array(
        'loop' => 3,
    );
    $r = new Response();
    $r->addSpeak($body, $attributes);
    echo ($r->toXML());
}
else
{
    echo "<p>Welcome to Plivo</p>";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package plivoexample;

import java.io.IOException;
import java.net.URLDecoder;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.plivo.helper.exception.PlivoException;
import com.plivo.helper.xml.elements.Message;
import com.plivo.helper.xml.elements.PlivoResponse;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

import com.plivo.helper.util.*;

public class validateSignature extends HttpServlet {
  private static final long serialVersionUID = 1L;
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
    String auth_token = "<auth_token>";
    String signature = req.getHeader("X-Plivo-Signature-V3");
    String nonce = req.getHeader("X-Plivo-Signature-V3-Nonce");
    String url = req.getRequestURL().toString();
    String method = req.getMethod();

    if(method == "GET") {
      try {
        Boolean isValid = Utils.validateSignatureV3(url, nonce, signature, auth_token, method);
        System.out.println("Valid : " + isValid);
      } catch (PlivoException e) {
        e.printStackTrace();
      }
    }
    else{
      try {
        Map<String, String> params = req.getParameterMap();
        Boolean isValid = Utils.validateSignatureV3(url, nonce, signature, auth_token, method, params);
        System.out.println("Valid : " + isValid);
      } catch (PlivoException e) {
        e.printStackTrace();
      }
    }

    PlivoResponse response = new PlivoResponse();
    Speak spk = new Speak("Hello, Welcome to Plivo");

    try {
      response.append(spk);
      System.out.println(response.toXML());
      resp.addHeader("Content-Type", "text/xml");
      resp.getWriter().print(response.toXML());
    } catch (PlivoException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) throws Exception {
    String port = System.getenv("PORT");
    if(port==null)
      port ="8000";
    Server server = new Server(Integer.valueOf(port));
    ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
    context.setContextPath("/");
    server.setHandler(context);
    context.addServlet(new ServletHolder(new  validateSignature()),"/speak");
    server.start();
    server.join();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"reflect"
	"unsafe"

	"github.com/plivo/plivo-go/v7"
	"github.com/plivo/plivo-go/v7/xml"
	"github.com/sirupsen/logrus"
)

type Message struct {
	Id   string `json:"id"`
	Name string `json:"name"`
}

func BytesToString(b []byte) string {
	bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	sh := reflect.StringHeader{bh.Data, bh.Len}
	return *(*string)(unsafe.Pointer(&sh))
}

func speak(w http.ResponseWriter, request *http.Request) {
	url := request.Host + request.RequestURI
	signature := request.Header.Get("X-Plivo-Signature-V3")
	nonce := request.Header.Get("X-Plivo-Signature-V3-Nonce")
	method := request.Method
	authToken := "<auth_token>"
	var valid bool
	if method == "GET" {
		valid = plivo.ValidateSignatureV3(url, nonce, method, signature, authToken)
	} else {
		parameters := make(map[string]string)
		b, err := ioutil.ReadAll(request.Body)
		defer request.Body.Close()
		if err != nil {
			http.Error(w, err.Error(), 500)
			return
		}

		var msg Message
		err = json.Unmarshal(b, &msg)
		if err != nil {
			http.Error(w, err.Error(), 500)
			return
		}

		var inInterface map[string]interface{}
		inrec, _ := json.Marshal(msg)
		json.Unmarshal(inrec, &inInterface)
		for field, val := range inInterface {
			parameters[field] = val.(string)
		}

		w.Header().Set("content-type", "text/xml")
		valid = plivo.ValidateSignatureV3(url, nonce, method, signature, authToken, parameters)
	}
	logrus.Info(valid)
	response := xml.ResponseElement{
		Contents: []interface{}{
			new(xml.SpeakElement).
				AddSpeak("Go Green, Go Plivo."),
		},
	}
	fmt.Fprintf(w, response.String())
}

func main() {
	http.HandleFunc("/speak/", speak)
	http.ListenAndServe(":5000", nil)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System;
using System.Collections.Generic;
using System.Diagnostics;
using RestSharp;
using Plivo.XML;
using Plivo;
using Nancy;

namespace plivo_dotnet_app
{
    public sealed class Program : NancyModule
    {
        public Program()
        {
            Get("/speak/", x =>
            {
                string signature = Request.Headers["X-Plivo-Signature-V3"].ToString();
                string nonce = Request.Headers["X-Plivo-Signature-V3_Nonce"].ToString();
                string auth_token = "<auth_token>";
                string method = Request.Method;
                string url = Request.Url;
                bool valid;
                Dictionary<string, string> parameters = new Dictionary<string, string>();
                valid = Plivo.Utilities.XPlivoSignatureV3.VerifySignature(url, nonce, signature, auth_token, method);
                Debug.WriteLine("Valid : " + valid);

                Plivo.XML.Response resp = new Plivo.XML.Response();
                resp.AddSpeak("Hello, Welcome to Plivo", parameters);
                Debug.WriteLine(resp.ToString());

                var output = resp.ToString();
                var res = (Nancy.Response) output;
                res.ContentType = "text/xml";
                return res;
            });

            Post<Response>("/speak/", x =>
            {
                string signature = Request.Headers["X-Plivo-Signature-V3"].ToString();
                string nonce = Request.Headers["X-Plivo-Signature-V3_Nonce"].ToString();
                string auth_token = "<auth_token>";
                string method = Request.Method;
                string url = Request.Url;
                bool valid;
                Dictionary<string, string> parameters = new Dictionary<string, string>();
                parameters = Request.Form;
                valid = Plivo.Utilities.XPlivoSignatureV3.VerifySignature(url, nonce, signature, auth_token, method, parameters);
                Debug.WriteLine("Valid : " + valid);

                Plivo.XML.Response resp = new Plivo.XML.Response();
                resp.AddSpeak("Hello, Welcome to Plivo", parameters);
                Debug.WriteLine(resp.ToString());

                var output = resp.ToString();
                var res = (Nancy.Response) output;
                res.ContentType = "text/xml";
                return res;
            });
        }
        
        static void Main(string[] args)
        {
            var p = new Program();
        }
    }
}

When someone makes a call to a Plivo number, Plivo requests the webhook associated with this code. The code runs and returns a Speak XML element if it can authenticate the request sent. If the validation fails the call will terminate, since no XML is returned to the Plivo server.

You may also write your own request validation code by generating request signatures based on the signature generation logic and validating them against the signatures passed by Plivo in the HTTP request headers.

Deprecation notice: X-Plivo-Signature-V2

If you’re still using Plivo’s V2 signatures to validate requests, we highly recommend switching over to X-Plivo-Signature-V3. V3 signatures offer improved protection against manipulation of POST request payloads en route.