Technical Documentation

HMAC Request Signing and Key Pairs

162 views 0

The Zephr Admin API is secured using key pair authentication. Once a key pair has been created against an Admin User, a request can be signed using an HMAC algorithm in order to allow the request to be executed with the role and identity of the Admin User who owns the keypair.

Creating a Keypair

In Zephr

Navigate to the Admin User Settings in the top right corner, and click Key Pairs.

Here you will see a list of the available Key Pairs for your admin user.

To create a key pair, click Issue Key Pair.

Key Pair Created

A modal will open showing your Access Key and Secret Key. Take note of these, as you will not be able to recover the secret key in the future.

Once you have saved these details, click Ok.

In Zephr Classic

Key pairs are managed under the user icon in the top right of the Zephr admin dashboard:

Click “Issue keypair” and take note of the secret key.

You will not be able to retrieve the secret key after it is initially displayed.

Managing Key Pairs

Keypairs created in the admin console allow an integration service to act on behalf of the user who generates them. For key pairs that are not linked to a user, contact support@zephr.com.

You can add notes to the key pair from the context menu in the list of key pairs.

You can also create keypairs via the REST API, if required:

POST /v3/admin/users/{user_id}/keypairs

Note that no body is required in this request.

The response will be:

{   
  "access_key": 
  "access key...",   "secret_key": "secret key...",   
  "message": "Keypair created: you will not be able to recover the secret, so take note of it" 
}

The secret key can never be recovered so it is important to record the payload from this request and store it securely.

Signing a request

To execute a secure request you must provide an Authorization header with a request signature:

GET /v3/users 
Authorization: ZEPHR-HMAC-{{ALGORITHM}} {{ACCESS_KEY}}:{{TIMESTAMP}}:{{NONCE}}:{{HASH}}

Where:

  • ALGORITHM is “SHA256”
  • ACCESS_KEY is the access key from the keypair
  • TIMESTAMP is the current number of milliseconds since the Epoc
  • NONCE is a random string (usually a number) that is unique – if you make two requests you must change the nonce between them
  • HASH is a hash generated by digesting the following inputs into an SHA-256 message-digest algorithm and outputting the resulting hex value:
    • the secret key from the keypair
    • the request’s body
    • the request’s path (not including the host, e.g. “/v3/users”)
    • the request query (not including the “?”)
    • the request method, in capitals, i.e. “POST”, “PUT”, “GET”, “DELETE”
    • the timestamp which must exactly match the TIMESTAMP part of the signature
    • the nonce, which must exactly match the NONCE part of the signature

Older request signatures were of the format: BLAIZE-HMAC-{{ALGORITHM}} {{ACCESS_KEY}}:{{TIMESTAMP}}:{{NONCE}}:{{HASH}}

This format did not include the query as part of the hash. Usage of legacy signatures is discouraged.

Reference implementations

Java

The Zephr HmacSigner is available as part of the Zephr API project. Please contact support@zephr.com to arrange access or otherwise you may copy and distribute the following code without licence.

package io.blaize.api.utilities.security;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;

import io.blaize.api.exception.HmacException;

public class HmacSigner {

    public static final String TWO_DIGIT_HEX_FORMAT = "%1$02x";
    private final String algorithm;

    public HmacSigner(String algorithm) {

        if ("SHA256".equals(algorithm)) {
            this.algorithm = "SHA-256";
        } else {
            this.algorithm = algorithm;
        }
    }

	public String signRequest(String secretKey, String body, String path, String query, String method,
                                    String timestamp, String nonce) throws HmacException {

        Objects.requireNonNull(secretKey);
        Objects.requireNonNull(body);
        Objects.requireNonNull(path);
        Objects.requireNonNull(query);
        Objects.requireNonNull(method);
        Objects.requireNonNull(timestamp);
        Objects.requireNonNull(nonce);

        try {
            MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
            messageDigest.update(secretKey.getBytes());
            messageDigest.update(body.getBytes());
            messageDigest.update(path.getBytes());
            messageDigest.update(query.getBytes());
            messageDigest.update(method.getBytes());
            messageDigest.update(timestamp.getBytes());
            messageDigest.update(nonce.getBytes());

            byte[] digest = messageDigest.digest();
            StringBuffer hash = new StringBuffer();
            for (byte digestByte : digest) {
                Integer unsignedInteger = new Integer(Byte.toUnsignedInt(digestByte));
                hash.append(String.format(TWO_DIGIT_HEX_FORMAT, unsignedInteger));
            }
            return hash.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new HmacException(e);
        }
    }
}

Then a signature can be used as follows:

The following code is non-normative and only intended as a reference.

String protocol = "http";
String host = "admin.test.blaize.io";
String path = "/v3/users";
String method = "POST";
String body = "{\"identifiers\": { \"email_address\": \"test@test.com\" }, \"validators\": { \"password\": \"sup3rsecre!10t\" }}";

String accessKey = "xyz";
String secretKey = loadSecretKeySecurely(accessKey);

String timestamp = String.valueOf(new Date().getTime());
String nonce = UUID.randomUUID().toString();
String query = "";
String hash = new HmacSigner("SHA-256").
		signRequest(secretKey, body, path, query, method, timestamp, nonce);

String authorizationHeaderValue = "ZEPHR-HMAC-SHA256 "
		+ accessKey + ":" + timestamp + ":" + nonce + ":" + hash;

// This is a standard library implementation for illustration only
HttpURLConnection connection = (HttpURLConnection) new URL(protocol + "://" + host + path + "?" + query).openConnection();
connection.setRequestMethod(method);
connection.addRequestProperty("Authorization", authorizationHeaderValue);
connection.addRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true);
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
outputStream.writeBytes(body);
outputStream.flush();
outputStream.close();

int status = connection.getResponseCode();
if (status >= 200 && status < 400) {
	System.out.println(new BufferedReader(new InputStreamReader(connection.getInputStream())).lines().collect(Collectors.joining("\n")));
} else {
	System.err.println(status);
}