Skip to content

Security

Dynata Signing Algorithm

All requests to Dynata’s Opportunity Registry and Respondent Gateway must be signed according to the Dynata signing algorithm. To sign a request, partners will need three key pieces of information:

  1. An access key provided to partners by Dynata. This value identifies the partner’s integration.

  2. A secret key provided to partners by Dynata. This value verifies the owner of the given access key. The secret key value must not be shared with anyone and will never be present in the actual request.

  3. An expiration timestamp that generated for each individual request. This timestamp must be formatted following the RFC3339 standard (e.g. 2021-01-01T08:09:10.001Z). A request will become invalid once the current time is greater than or equal to the specified expiration value.

Signing String

Every signature begins with a signing string. The signing string will be used in the first step of generating a signature. There are two type of signing strings, API request and URL.

API Request Signing String

Each Opportunity Registry and Respondent Gateway endpoint requires a SHA256 lowercase hex encoded digest of the request body as the signing string. If the request does not contain a body, use a SHA256 lowercase hex encoded digest of an empty string as the signing string.

Examples

JSON Payload
request body
{
"key": "value"
}

Example command to create signing string using the request body.

terminal
REQUEST_BODY='{
"key": "value"
}'
echo -n $REQUEST_BODY | openssl sha256

signing string
2715faa1cb1f76e0246b1f71095d163ba9a23afebfb51db8d52c2e0a50da6d1f
Empty String

Example command to create signing string using an empty string.

terminal
echo -n "" | openssl sha256

signing string
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

URL Signing String

To sign URLs a canonical query string must be constructed as the string. To construct the canonical query string, follow these steps:

  1. Sort the parameter names by character code point in ascending order. Parameters with duplicate names should be sorted by value.

    Example

    A parameter name that begins with the uppercase letter Z precedes a parameter name that begins with a lowercase letter b.

  2. URI-encode each parameter name and value according to the following rules:

    • Do not URI-encode any of the unreserved characters that RFC 3986 defines:

      • A-Z, a-z, and 0-9
      • hyphen ( - )
      • underscore ( _ )
      • period ( . )
      • tilde ( \~ ).
    • Percent-encode all other characters with %XY, where X and Y are hexadecimal characters (0-9 and uppercase A-F). Extended UTF-8 characters must be in the form %XY%ZA%BC.

      Example

      The space ( “ ) character must be encoded as %20 (not using +, as some encoding schemes do).

    • Double-encode any equals ( = ) characters in the parameter values.

  3. Build the canonical query string by starting with the first parameter name in the sorted list.

  4. For each parameter, append the URI-encoded parameter name, followed by the equals sign character ( = ), followed by the URI-encoded parameter value. Use an empty string for parameters that have no value.

  5. Append the ampersand character ( & ) after each parameter value, except for the last value in the list.

  6. Lastly, the canonical query string must then be hashed using the SHA256 algorithm. The hash digest must be represented as a lowercase hexadecimal string. This will be the URL signing string.

Example

Inbound URL with query string parameters

https://partn.er?ctx=context123&respondent_id=user123&language=en&expiration=2021-10-19T17:48:36.480Z&access_key=1234&Zeta=encode €xample~v@lue&dupes=this=two&dupes=2&null=&

Query string parameters sorted by character code point.

Zeta=encode, €xample~v@lue
access_key=1234
ctx=context123
dupes=2
dupes=this=two
expiration=2021-10-19T17:48:36.480Z
language=en
null=
respondent_id=user123

Encoded special characters.

Zeta=encode%2C%E2%82%ACxample~v%40lue
access_key=1234
ctx=context123
dupes=2
dupes=this==two
expiration=2021-10-19T17%3A48%3A36.480Z
language=en
null=
respondent_id=user123

The canonical query string.

Zeta=encode%2C%E2%82%ACxample~v%40lue&access_key=1234&ctx=context123&dupes=2&dupes=this==two&expiration=2021-10-19T17%3A48%3A36.480Z&language=en&null=&respondent_id=user123&

Example command to create signing string using the canonical query string.

Terminal window
CANONICAL_QUERY_STRING="Zeta=encode%2C%E2%82%ACxample~v%40lue&access_key=1234&ctx=context123&dupes=2&dupes=this==two&expiration=2021-10-19T17%3A48%3A36.480Z&language=en&null=&respondent_id=user123&"
echo -n $CANONICAL_QUERY_STRING | openssl sha256
# output: signing string
069a66bbd3a7ef648fcd67f557ec74df40a1eb381b7c151a4bf23335a6eeeddf

Creating a Signature

The signature will be used to sign all opportunity entry links, as well as any API calls made to the Opportunity Registry or Respondent Gateway endpoints.

The following are step-by-step instructions for creating the request signature:

  1. First generate the appropriate signing string, for an API request or URL.

  2. The signing string must then be hashed using HMAC-SHA256 using the value of the expiration parameter as the key. The hash digest must be represented as a lowercase hexadecimal string.

  3. The digest from step 2 must then be hashed using HMAC-SHA256 using the value of the access key parameter as the key. The hash digest must be represented as a lowercase hexadecimal string.

  4. The digest from step 3 must then be hashed using HMAC-SHA256 using the secret key parameter as the key. The hash digest must be represented as a lowercase hexadecimal string. This value is the request signature.

Signing Methodology Code Examples

API Request Signature

using System.Security.Cryptography;
using System.Text;
namespace example
{
public static class Extensions
{
public static string ToHexLower(this byte[] b)
{
var sb = new StringBuilder();
foreach (var t in b)
{
sb.Append($"{t:x2}");
}
return sb.ToString();
}
public static byte[] Sha256(this byte[] b)
{
using var sha256 = SHA256.Create();
return sha256.ComputeHash(b);
}
public static byte[] HmacSha256(this byte[] b, byte[] key)
{
using var hmac = new HMACSHA256(key);
return hmac.ComputeHash(b);
}
public static byte[] Bytes(this string s)
{
return Encoding.UTF8.GetBytes(s);
}
public static string Sign(this string s, string expiration, string accessKey, string secretKey)
{
var first = s.Bytes().HmacSha256(expiration.Bytes()).ToHexLower();
var second = first.Bytes().HmacSha256(accessKey.Bytes()).ToHexLower();
return second.Bytes().HmacSha256(secretKey.Bytes()).ToHexLower();
}
}
class Program
{
static void Main(string[] args)
{
var signingString = "this is a basic signing string".Bytes().Sha256().ToHexLower();
var signature = signingString.Sign("2021-12-31T01:01:01.001Z", "access_key", "some_secret_key");
}
}
}
package example
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func Sha256(message []byte) []byte {
digest := sha256.Sum256(message)
return digest[:]
}
func Hmac(message []byte, key []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(message)
return h.Sum(nil)
}
func ToHexLower(message []byte) string {
return hex.EncodeToString(message)
}
func sign(signingString string, expiration string, accessKey string, secretKey string) string {
first := ToHexLower(Hmac([]byte(signingString), []byte(expiration)))
second := ToHexLower(Hmac([]byte(first), []byte(accessKey)))
return ToHexLower(Hmac([]byte(second), []byte(secretKey)))
}
func main() {
signingString := ToHexLower(Sha256([]byte("this is a basic signing string")))
signature := sign(signingString, "2021-12-31T01:01:01.001Z", "access_key", "some_secret_key")
}
package com.company;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Main {
public static byte[] hmacSha256(byte[] message, byte[] key) throws InvalidKeyException {
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
try {
Mac mac = Mac.getInstance(keySpec.getAlgorithm());
mac.init(keySpec);
return mac.doFinal(message);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static byte[] sha256(byte[] message) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(message);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
static final char[] hexArray = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0, v; i < bytes.length; i++) {
v = bytes[i] & 0xFF;
hexChars[i * 2] = hexArray[v >>> 4];
hexChars[i * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static String sign(
String signingString,
String expiration,
String accessKey,
String secretKey
) throws InvalidKeyException {
String first = bytesToHex(hmacSha256(
signingString.getBytes(StandardCharsets.UTF_8),
expiration.getBytes(StandardCharsets.UTF_8)
));
String second = bytesToHex(hmacSha256(
first.getBytes(StandardCharsets.UTF_8),
accessKey.getBytes(StandardCharsets.UTF_8)
));
return bytesToHex(hmacSha256(
second.getBytes(StandardCharsets.UTF_8),
secretKey.getBytes(StandardCharsets.UTF_8)
));
}
public static void main(String[] args) {
String signingString = bytesToHex(sha256("this is a basic signing string".getBytes(StandardCharsets.UTF_8)));
try {
String signature = sign(signingString, "2021-12-31T01:01:01.001Z", "access_key", "some_secret_key");
} catch (InvalidKeyException e) {
System.out.println(e.getMessage());
}
}
}
const crypto = require("crypto");
function sha256(s) {
const hasher = crypto.createHash("sha256");
hasher.update(s);
return hasher.digest("hex");
}
function hmac(s, k) {
const hasher = crypto.createHmac("sha256", k);
hasher.update(s);
return hasher.digest("hex");
}
function sign(signing_string, expiration, access_key, secret_key) {
const first = hmac(signing_string, expiration);
const second = hmac(first, access_key);
return hmac(second, secret_key);
}
function main() {
var signing_string = sha256("this is a basic signing string");
var signature = sign(signing_string, "2021-12-31T01:01:01.001Z", "access_key", "some_secret_key");
}
function sign(string $signingString, string $accessKey, string $secretKey, string $expiration): string {
$first = \hash_hmac('sha256', $signingString, $expiration);
$second = \hash_hmac('sha256', $first, $accessKey);
$signature = \hash_hmac('sha256', $second, $secretKey);
return $signature;
}
$signingString = \hash('sha256', 'this is a basic signing string');
$signature = sign($signingString, 'access_key', 'some_secret_key', '2021-12-31T01:01:01.001Z');
import hmac
import hashlib
def digest(signing_key: str, message: str, encoding='utf-8') -> str:
_hmac = hmac.new(
key=bytes(signing_key, encoding=encoding),
msg=bytes(message, encoding=encoding),
digestmod=hashlib.sha256
)
return _hmac.hexdigest()
def sha256(message: str) -> str:
return hashlib.sha256(bytes(message, encoding='utf-8')).hexdigest()
def sign(signing_string: str,
expiration: str,
access_key: str,
secret_key: str) -> str:
first = digest(expiration, signing_string)
second = digest(access_key, first)
final = digest(secret_key, second)
return final
def main():
signing_string = sha256('this is a basic signing string')
signature = sign(signing_string,
'2021-12-31T01:01:01.001Z',
'access_key',
'some_secret_key')
print(signature)

URL Signature

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Linq;
namespace example
{
public static class Extensions
{
public static string ToHexLower(this byte[] b)
{
var sb = new StringBuilder();
foreach (var t in b)
{
sb.Append($"{t:x2}");
}
return sb.ToString();
}
private static readonly char[] UpperHex = "0123456789ABCDEF".ToArray();
public static string ToHexUpper(this char c)
{
var b = Encoding.UTF8.GetBytes(new[] { c });
var output = new char[b.Length * 2];
for (var i = 0; i < b.Length; i++)
{
output[i * 2] = UpperHex[(b[i] >> 4) & 0xF];
output[i * 2 + 1] = UpperHex[b[i] & 0xF];
}
return new string(output);
}
public static byte[] Sha256(this byte[] b)
{
using var sha256 = SHA256.Create();
return sha256.ComputeHash(b);
}
private static byte[] HmacSha256(this byte[] b, byte[] key)
{
using var hmac = new HMACSHA256(key);
return hmac.ComputeHash(b);
}
public static byte[] Bytes(this string s)
{
return Encoding.UTF8.GetBytes(s);
}
public static string Sign(this string s, string expiration, string accessKey, string secretKey)
{
var first = s.Bytes().HmacSha256(expiration.Bytes()).ToHexLower();
var second = first.Bytes().HmacSha256(accessKey.Bytes()).ToHexLower();
return second.Bytes().HmacSha256(secretKey.Bytes()).ToHexLower();
}
public static TimeSpan Seconds(this int s)
{
return TimeSpan.FromSeconds(s);
}
public static string ToRfc3339(this DateTime d)
{
return d.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", DateTimeFormatInfo.InvariantInfo);
}
}
public class Encoder
{
private readonly HashSet<char> _unreservedCharacters;
public Encoder()
{
var hs = new HashSet<char>();
for (var c = 'A'; c <= 'Z'; c++)
{
hs.Add(c);
}
for (char c = 'a'; c <= 'z'; c++)
{
hs.Add(c);
}
for (char c = '0'; c <= '9'; c++)
{
hs.Add(c);
}
hs.Add('-');
hs.Add('.');
hs.Add('_');
hs.Add('~');
_unreservedCharacters = hs;
}
public string Encode(string s)
{
StringBuilder sb = new StringBuilder();
foreach (char c in s.ToArray())
{
if (_unreservedCharacters.Contains(c))
{
sb.Append(c);
continue;
}
sb.Append('%');
sb.Append(c.ToHexUpper());
}
return sb.ToString();
}
}
public class UrlSigner
{
private readonly string _accessKey;
private readonly string _secretKey;
private readonly Encoder _encoder;
public UrlSigner(string accessKey, string secretKey)
{
_accessKey = accessKey;
_secretKey = secretKey;
_encoder = new Encoder();
}
public string Sign(string url, TimeSpan ttl)
{
var uri = new Uri(url);
var queryParams = HttpUtility.ParseQueryString(uri.Query);
var hasParams = queryParams.Count > 0;
var exp = (DateTime.Now + ttl).ToRfc3339();
var additionalParams = new NameValueCollection
{
{ "expiration", exp },
{ "access_key", _accessKey }
};
queryParams.Add("expiration", exp);
queryParams.Add("access_key", _accessKey);
var signingParams = (
from key in queryParams.AllKeys
from v in queryParams.GetValues(key)
select $"{_encoder.Encode(key)}={_encoder.Encode(v.Replace("=", "%3D"))}"
).OrderBy(s => s, StringComparer.Ordinal).ToArray();
var signingQueryString = string.Join("&", signingParams);
var signingString = signingQueryString.Bytes().Sha256().ToHexLower();
var signature = signingString.Sign(exp, _accessKey, _secretKey);
additionalParams.Add("signature", signature);
var outputParams = (
from key in additionalParams.AllKeys
from v in additionalParams.GetValues(key)
select $"{_encoder.Encode(key)}={_encoder.Encode(v)}"
).ToArray();
return url + (hasParams ? "&" : "?") + string.Join("&", outputParams);
}
}
class Program
{
public static void Main(string[] args)
{
const string url =
"https://respondent.qa-rex.dynata.com/start?ctx=1120e821-a795-4358-abb1-4cebbc87ae0a&language=en";
var signer = new UrlSigner("some_access_key", "some_secret_key");
var signedUri = signer.Sign(url, 1000.Seconds());
Console.Out.WriteLine(signedUri);
}
}
}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"net/url"
"sort"
"strings"
"time"
)
const RFC3339Milli = "2006-01-02T15:04:05.000Z07:00"
type Expiration struct {
inner time.Time
}
func ExpireIn(d time.Duration) Expiration {
return Expiration{inner: time.Now().UTC().Add(d)}
}
func (t *Expiration) String() string {
return t.inner.Format(RFC3339Milli)
}
func Sha256(message []byte) []byte {
digest := sha256.Sum256(message)
return digest[:]
}
func Hmac(message []byte, key []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(message)
return h.Sum(nil)
}
func ToHexLower(message []byte) string {
return hex.EncodeToString(message)
}
func sign(signingString string, expiration string, accessKey string, secretKey string) string {
first := ToHexLower(Hmac([]byte(signingString), []byte(expiration)))
second := ToHexLower(Hmac([]byte(first), []byte(accessKey)))
return ToHexLower(Hmac([]byte(second), []byte(secretKey)))
}
type UrlSigner struct {
accessKey string
secretKey string
}
var upperhex = []rune("0123456789ABCDEF")
func ToHexUpper(b []byte) []rune {
o := make([]rune, len(b)*2)
for i := 0; i < len(b); i++ {
o[i*2] = upperhex[(b[i]>>4)&0xF]
o[i*2+1] = upperhex[b[i]&0xF]
}
return o
}
var unreservedCharacters map[int32]struct{}
func init() {
m := make(map[int32]struct{})
for i := 'A'; i <= 'Z'; i++ {
m[i] = struct{}{}
}
for i := 'a'; i <= 'z'; i++ {
m[i] = struct{}{}
}
for i := '0'; i <= '9'; i++ {
m[i] = struct{}{}
}
m['-'] = struct{}{}
m['.'] = struct{}{}
m['_'] = struct{}{}
m['~'] = struct{}{}
unreservedCharacters = m
}
func Encode3986(s string) string {
var sb strings.Builder
for idx := 0; idx < len(s); {
r, rWidth := utf8.DecodeRuneInString(s[idx:])
idx += rWidth
if _, ok := unreservedCharacters[r]; ok {
sb.WriteRune(r)
continue
}
sb.WriteRune('%')
b := make([]byte, rWidth)
i := utf8.EncodeRune(b, r)
hexRunes := ToHexUpper(b[:i])
for _, hexRune := range hexRunes {
sb.WriteRune(hexRune)
}
}
return sb.String()
}
func ToCanonicalQueryString(v url.Values) string {
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
i := 0
var sb strings.Builder
for _, k := range keys {
if i > 0 {
sb.WriteRune('&')
}
name := k
values := v[k]
for j := 0; j < len(values); j++ {
value := strings.ReplaceAll(values[j], "=", "%3D")
sb.WriteString(Encode3986(name))
sb.WriteRune('=')
sb.WriteString(Encode3986(value))
i++
}
}
return sb.String()
}
func ToQueryString(v url.Values) string {
i := 0
var sb strings.Builder
for name, values := range v {
if i > 0 {
sb.WriteRune('&')
}
for j := 0; j < len(values); j++ {
sb.WriteString(Encode3986(name))
sb.WriteRune('=')
sb.WriteString(Encode3986(values[j]))
i++
}
}
return sb.String()
}
func (s *UrlSigner) Sign(uri *string, expiration Expiration) error {
u, err := url.Parse(*uri)
if err != nil {
return fmt.Errorf("cannot sign invalid uri: %w", err)
}
q := u.Query()
hasParams := len(q) > 0
exp := expiration.String()
q.Add("expiration", exp)
q.Add("access_key", s.accessKey)
additionalParams := make(map[string][]string, 3)
additionalParams["expiration"] = []string{exp}
additionalParams["access_key"] = []string{s.accessKey}
qs := ToCanonicalQueryString(q)
ss := ToHexLower(Sha256([]byte(qs)))
sig := sign(ss, exp, s.accessKey, s.secretKey)
additionalParams["signature"] = []string{sig}
joiner := "&"
if !hasParams {
joiner = "?"
}
*uri = *uri + joiner + ToQueryString(additionalParams)
return nil
}
func NewUrlSigner(accessKey, secretKey string) *UrlSigner {
return &UrlSigner{
accessKey: accessKey,
secretKey: secretKey,
}
}
func main() {
exp := ExpireIn(30 * time.Second)
signer := NewUrlSigner("some_access_key", "some_secret_key")
uri := "https://respondent.qa-rex.dynata.com/start?ctx=1120e821-a795-4358-abb1-4cebbc87ae0a&language=en"
err := signer.Sign(&uri, exp)
if err != nil {
log.Fatalf("problem when signing: %s", err)
}
fmt.Println(uri)
}
package com.company;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.CharArrayWriter;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiFunction;
public class Main {
static final char[] hexArray = "0123456789abcdef".toCharArray();
public static byte[] hmacSha256(byte[] message, byte[] key) throws InvalidKeyException {
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
try {
Mac mac = Mac.getInstance(keySpec.getAlgorithm());
mac.init(keySpec);
return mac.doFinal(message);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static byte[] sha256(byte[] message) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(message);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0, v; i < bytes.length; i++) {
v = bytes[i] & 0xFF;
hexChars[i * 2] = hexArray[v >>> 4];
hexChars[i * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static String sign(String signingString, String accessKey, String secretKey, String expiration) throws InvalidKeyException {
String first = bytesToHex(hmacSha256(signingString.getBytes(StandardCharsets.UTF_8), expiration.getBytes(StandardCharsets.UTF_8)));
String second = bytesToHex(hmacSha256(first.getBytes(StandardCharsets.UTF_8), accessKey.getBytes(StandardCharsets.UTF_8)));
return bytesToHex(hmacSha256(second.getBytes(StandardCharsets.UTF_8), secretKey.getBytes(StandardCharsets.UTF_8)));
}
public static void parseQuery(String q, Map<String, String> m) {
String[] pairs = q.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
String name = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
m.put(name, value);
}
}
public static String toQuery(Map<String, String> m, BiFunction<String, Charset, String> encoder) {
StringBuilder builder = new StringBuilder();
int i = 0;
for (Map.Entry<String, String> entry : m.entrySet()) {
if (i > 0) builder.append("&");
String name = encoder.apply(entry.getKey(), StandardCharsets.UTF_8);
String value = encoder.apply(entry.getValue().replace("=", "%3D"), StandardCharsets.UTF_8);
builder.append(name).append("=").append(value);
i += 1;
}
return builder.toString();
}
public static void main(String[] args) {
String url = "https://respondent.qa-rex.dynata.com/start?ctx=521288b7-0a8a-476a-81ba-4ed61a770fb7&language=en&respondent_id=abc123";
UrlSigner signer = new UrlSigner("some_access_key", "some_secret_key");
try {
String signedUrl = signer.signUrl(url, Duration.ofSeconds(10));
System.out.println(signedUrl);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public static class UriEncoder {
/*
This class will encode values according to RFC 3986.
Java's URLEncoder encodes values according to RFC 2396.
*/
static final char[] upperhex = "0123456789ABCDEF".toCharArray();
static BitSet unreservedCharacters;
static {
unreservedCharacters = new BitSet(256);
int i;
for (i = 'a'; i <= 'z'; i++) {
unreservedCharacters.set(i);
}
for (i = 'A'; i <= 'Z'; i++) {
unreservedCharacters.set(i);
}
for (i = '0'; i <= '9'; i++) {
unreservedCharacters.set(i);
}
unreservedCharacters.set('-');
unreservedCharacters.set('_');
unreservedCharacters.set('.');
unreservedCharacters.set('*');
unreservedCharacters.set('~');
}
static void toHex(StringBuilder out, byte b) {
char ch = upperhex[(b >> 4) & 0xF];
out.append(ch);
ch = upperhex[b & 0xF];
out.append(ch);
}
public static String encode(String s, Charset charset) {
Objects.requireNonNull(charset, "charset");
StringBuilder out = new StringBuilder(s.length());
CharArrayWriter charArrayWriter = new CharArrayWriter();
for (int i = 0; i < s.length(); ) {
int c = s.charAt(i);
if (unreservedCharacters.get(c)) {
out.append((char) c);
i++;
continue;
}
do {
charArrayWriter.write(c);
i++;
} while (i < s.length() && !unreservedCharacters.get((c = s.charAt(i))));
charArrayWriter.flush();
String str = charArrayWriter.toString();
byte[] ba = str.getBytes(charset);
for (byte b : ba) {
out.append('%');
toHex(out, b);
}
charArrayWriter.reset();
}
return out.toString();
}
}
public static class UrlSigner {
private final String accessKey;
private final String secretKey;
public UrlSigner(String accessKey, String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}
public String signUrl(String url, Duration expiration) throws Exception {
return this.signUrl(url, expiration, UriEncoder::encode);
}
public String signUrl(String url, Duration expiration, BiFunction<String, Charset, String> encoder) throws Exception {
URL aUrl = new URL(url);
Map<String, String> orderedQueryParams = new TreeMap<>();
parseQuery(aUrl.getQuery(), orderedQueryParams);
boolean hasParameters = orderedQueryParams.size() > 0;
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
String exp = now.plus(expiration).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
Map<String, String> additionalQueryParams = new HashMap<>();
additionalQueryParams.put("expiration", exp);
orderedQueryParams.put("expiration", exp);
additionalQueryParams.put("access_key", accessKey);
orderedQueryParams.put("access_key", accessKey);
String orderedQueryString = toQuery(orderedQueryParams, encoder);
byte[] digest = sha256(orderedQueryString.getBytes(StandardCharsets.UTF_8));
String signingString = bytesToHex(digest);
String signature = sign(signingString, accessKey, secretKey, exp);
additionalQueryParams.put("signature", signature);
String additionalQuery = toQuery(additionalQueryParams, encoder);
return url + (hasParameters ? "&" : "?") + additionalQuery;
}
}
}
const url = require("url");
const crypto = require("crypto");
function hash(s) {
const hasher = crypto.createHash("sha256");
hasher.update(s);
return hasher.digest("hex");
}
function hmac(s, k) {
const hasher = crypto.createHmac("sha256", k);
hasher.update(s);
return hasher.digest("hex");
}
function encode(s) {
return encodeURIComponent(s)
.replace(/[!*'()]/g, (c) => {
return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;
});
}
function compile_params(q, key_func = (k) => k, value_func = (v) => v) {
return (s, k) => {
const encoded_name = key_func(k);
const encoded_value = value_func(q[k].toString());
return `${s}&${encoded_name}=${encoded_value}`;
};
}
function sign(signing_string, access_key, secret_key, expiration) {
const first = hmac(signing_string, expiration);
const second = hmac(first, access_key);
return hmac(second, secret_key);
}
class UrlSigner {
constructor(access_key, secret_key) {
this.access_key = access_key;
this.secret_key = secret_key;
}
sign(uri, millis, encode_func = encode) {
const u = url.parse(uri, true);
let q = u.query;
const has_parameters = Object.keys(q).length > 0;
const now = Date.now();
const d = new Date(now + millis);
const exp = d.toISOString();
const additional_params = {
"expiration": exp,
"access_key": this.access_key,
};
q = Object.assign(q, additional_params);
const ordered_query = Object.keys(q)
.sort()
.reduce(compile_params(q, encode, (v) => encode(v.replace("=", "%3D"))), "")
.substring(1);
const signing_string = hash(ordered_query);
additional_params["signature"] = sign(signing_string, this.access_key, this.secret_key, exp);
const new_query = Object.keys(additional_params)
.reduce(compile_params(additional_params, encode_func, encode_func), "")
.substring(1);
return `${uri}${has_parameters ? "&" : "?"}${new_query}`;
}
}
const end_link = "https://respondent.qa-rex.dynata.com/start?ctx=521288b7-0a8a-476a-81ba-4ed61a770fb7&language=en&respondent_id=abc123";
const signer = new UrlSigner("some_access_key", "some_secret_key");
const signed_url = signer.sign(end_link, 10000);
console.log(signed_url);
<?php
function sign(string $signingString, string $accessKey, string $secretKey, string $expiration): string
{
$first = \hash_hmac('sha256', $signingString, $expiration);
$second = \hash_hmac('sha256', $first, $accessKey);
return \hash_hmac('sha256', $second, $secretKey);
}
class UrlSigner
{
private $accessKey;
private $secretKey;
function __construct(string $accessKey, string $secretKey)
{
$this->accessKey = $accessKey;
$this->secretKey = $secretKey;
}
function sign(string $url, \DateInterval $ttl): string
{
$expiration = (new \DateTime())->add($ttl)->format(\DATE_RFC3339_EXTENDED);
$parts = \parse_url($url);
\parse_str($parts['query'], $queryParams);
$hasParams = count($queryParams);
$queryParams['access_key'] = $this->accessKey;
$queryParams['expiration'] = $expiration;
$additionalParams = [
'access_key' => $this->accessKey,
'expiration' => $expiration
];
foreach ($queryParams as $key => $value) {
$queryParams[$key] = \str_replace('=', '%3D', $value);
}
\ksort($queryParams);
$canonicalQuery = \http_build_query($queryParams, "", "&", PHP_QUERY_RFC3986);
$signature = sign(\hash('sha256', $canonicalQuery), $this->accessKey, $this->secretKey, $expiration);
$additionalParams['signature'] = $signature;
$additionalQuery = \http_build_query($additionalParams, "", "&", PHP_QUERY_RFC3986);
return $url . ($hasParams ? '&' : '?') . $additionalQuery;
}
}
$urlSigner = new UrlSigner('some_access_key', 'some_secret_key');
$url = 'https://respondent.qa-rex.dynata.com/start?ctx=1120e821-a795-4358-abb1-4cebbc87ae0a&language=en';
$ttl = \DateInterval::createFromDateString('30 seconds');
$signedUrl = $urlSigner->sign($url, $ttl);
echo $signedUrl;
import hashlib
import hmac
from datetime import datetime, timedelta
from urllib import parse
from typing import List, Tuple, Callable
class Timestamp:
def __init__(self, d: datetime):
self._inner = d
def to_rfc3339(self) -> str:
return self._inner.isoformat(timespec="milliseconds") + 'Z'
def parse_timestamp(string: str) -> Timestamp:
return Timestamp(datetime.strptime(string, '%Y-%m-%dT%H:%M:%S.%fZ'))
def timestamp_from_secs(secs: int) -> Timestamp:
return Timestamp((datetime.utcnow() + timedelta(seconds=secs)))
def digest(signing_key: str, message: str, encoding='utf-8') -> str:
_hmac = hmac.new(
bytes(signing_key, encoding=encoding),
bytes(message, encoding=encoding),
hashlib.sha256
)
return _hmac.hexdigest()
def sha256(message: str) -> str:
return hashlib.sha256(bytes(message, encoding='utf-8')).hexdigest()
def encode(s: List[Tuple[str, str]]) -> str:
return parse.urlencode(s, quote_via=parse.quote)
def sign(access_key: str,
secret_key: str,
expiration: str,
signing_string: str = '') -> str:
first = digest(expiration, signing_string)
second = digest(access_key, first)
final = digest(secret_key, second)
return final
class UrlSigner:
def __init__(self, access_key: str, secret_key: str):
self._access_key = access_key
self._secret_key = secret_key
def sign(self,
url: str,
expiration: Timestamp,
encode_func: Callable[[List[Tuple[str, str]]], str] = encode) -> str:
parsed_url = parse.urlparse(url)
query = parse.parse_qsl(parsed_url.query)
has_parameters = len(query) > 0
exp_str = expiration.to_rfc3339()
additional_params = [
('expiration', exp_str),
('access_key', self._access_key)
]
query.extend(additional_params)
for i, t in enumerate(query):
# double-encode equal character in parameter value
query[i] = (t[0], t[1].replace("=", "%3D"))
def sort_query(x: Tuple[str, str]) -> str:
return x[0]
ordered_query = sorted(query, key=sort_query)
ordered_query_string = parse.urlencode(ordered_query, quote_via=parse.quote)
signing_string = sha256(ordered_query_string)
signature = sign(self._access_key,
self._secret_key,
exp_str,
signing_string)
additional_params.append(('signature', signature))
query_string = encode_func(additional_params)
output = url
if has_parameters:
output += "&"
else:
output += "?"
return output + query_string
if __name__ == '__main__':
url = 'https://respondent.qa-rex.dynata.com/start?ctx=7c26bf58-43db-4370-977d-d14fa4356930&language=es'
signer = UrlSigner('some_access_key', 'some_secret_key')
signed_url = signer.sign(url, timestamp_from_secs(20))
print(signed_url)

Using the signature

For API requests pass the generated signature as the dynata-signature, and for URLs append the signature as a query string parameter by the same name.

Signed Request

Pass the signature and other generated parameters in the request via the header fields.

POST /path
Host: partn.er
dynata-access-key: partner_access_key
dynata-expiration: request_expirtation
dynata-signature: signature
{
// request body
}

Signed URL

Append the generated signature to the URL’s query string in a parameter named signature.

https://partn.er?a=1&access_key=partner_access_key&expiration=url_expiration&signature=signature

Interactive Signer

The interactive signing tool can help partners generate and test signature construction.