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:
-
An
access key
provided to partners by Dynata. This value identifies the partner’s integration. -
A
secret key
provided to partners by Dynata. This value verifies the owner of the givenaccess key
. Thesecret key
value must not be shared with anyone and will never be present in the actual request. -
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 specifiedexpiration
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
{ "key": "value"}
Example command to create signing string
using the request body
.
REQUEST_BODY='{ "key": "value"}'echo -n $REQUEST_BODY | openssl sha256
2715faa1cb1f76e0246b1f71095d163ba9a23afebfb51db8d52c2e0a50da6d1f
Empty String
Example command to create signing string
using an empty string
.
echo -n "" | openssl sha256
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:
-
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 witha
lowercase letterb
. -
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
, and0-9
- hyphen (
-
) - underscore (
_
) - period (
.
) - tilde (
\~
).
-
Percent-encode all other characters with
%XY
, whereX
andY
are hexadecimal characters (0-9
and uppercaseA-F
). ExtendedUTF-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.
-
-
Build the canonical query string by starting with the first parameter name in the sorted list.
-
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. -
Append the ampersand character (
&
) after each parameter value, except for the last value in the list. -
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@lueaccess_key=1234ctx=context123dupes=2dupes=this=twoexpiration=2021-10-19T17:48:36.480Zlanguage=ennull=respondent_id=user123
Encoded special characters.
Zeta=encode%2C%E2%82%ACxample~v%40lueaccess_key=1234ctx=context123dupes=2dupes=this==twoexpiration=2021-10-19T17%3A48%3A36.480Zlanguage=ennull=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.
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 string069a66bbd3a7ef648fcd67f557ec74df40a1eb381b7c151a4bf23335a6eeeddf
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:
-
First generate the appropriate
signing string
, for an API request or URL. -
The
signing string
must then be hashed usingHMAC-SHA256
using the value of theexpiration
parameter as the key. The hash digest must be represented as a lowercase hexadecimal string. -
The digest from step 2 must then be hashed using
HMAC-SHA256
using the value of theaccess key
parameter as the key. The hash digest must be represented as a lowercase hexadecimal string. -
The digest from step 3 must then be hashed using
HMAC-SHA256
using thesecret key
parameter as the key. The hash digest must be represented as a lowercase hexadecimal string. This value is the requestsignature
.
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 hmacimport 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 hashlibimport hmac
from datetime import datetime, timedeltafrom urllib import parsefrom 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 /pathHost: partn.erdynata-access-key: partner_access_keydynata-expiration: request_expirtationdynata-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.