Authentication Overview with Http Signatures

Generation and use of authentication tokens on Layer1

This guide explains how to generate and use RSA-based HTTP signatures to authenticate API requests securely. It ensures that requests are tamper-proof and trusted, relying only on cryptographic keys. This approach ensures more secure interactions with all payments API.

1. Generating RSA Keys for API Signatures

To perform signed HTTP requests, you will need to create an RSA key pair: a private key to sign your requests and a public key to register or verify the signature. Follow these steps:

  1. Generate the Private Key

Run the following command to generate a private key:

openssl genrsa -out api-client-key.pem
  1. Extract the Public Key

Use this command to extract the public key from the private key:

openssl rsa -in api-client-key.pem -pubout -out api-client-public-key.pem

The private key stays securely with your application, while the public key is registered with Layer1 to authenticate your requests.

Managing Keys

Storage & Security: Save the private key in a secure place where only your application can access it. The public key will be used to register your client or share it during setup.

2. Signing HTTP Requests

Each request is signed using a combination of the HTTP method, request URL, request body, and a timestamp. The SHA-256 digest of the request body ensures that the data has not been tampered with, and the signature ensures that the request came from a trusted source. A Date header is also included to mitigate replay attacks by ensuring the request is recent.

Code Example:

import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class HttpSigner {

    private static final String SIGNATURE_ALGORITHM = "rsa-v1_5-sha256";
    private static final String DIGEST_ALGORITHM = "sha-256";
    private final PrivateKey privateKey;
    private final String clientId;

    /**
     * Constructor to load the private key from a Base64-encoded string.
     *
     * @param base64PrivateKey Base64-encoded private key string
     */
    public HttpSigner(String base64PrivateKey, String clientId) {
        this.clientId = clientId;	 
        this.privateKey = loadPrivateKey(base64PrivateKey);
    }

    /**
     * Load a PrivateKey object from a Base64-encoded string.
     */
    private PrivateKey loadPrivateKey(String key) {
        try {
            String preparedKey = prepareKey(key);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePrivate(
                new PKCS8EncodedKeySpec(Base64.getDecoder().decode(preparedKey))
            );
        } catch (Exception e) {
            throw new RuntimeException("Failed to load private key", e);
        }
    }

    /**
     * This method builds the necessary signature headers and returns them as a map.
     *
     * @param url     The full URL of the request
     * @param payload The body of the request (if any, POST, etc)
     * @param method  The HTTP method of the request
     * @return A map containing the signature headers
     */
    public Map<String, String> buildHeaders(String url, String payload, String method) {
        Map<String, String> headerParams = new HashMap<>();

        String contentDigest = null;
        if (!Objects.isNull(payload) && !payload.isEmpty()) {
            try {
                contentDigest = createDigest(DIGEST_ALGORITHM, payload);
                headerParams.put("Content-Digest", contentDigest);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("Failed to create digest", e);
            }
        }

        String signatureParameters = createSignatureParameters(contentDigest);
        headerParams.put("Signature-Input", "sig=" + signatureParameters);

        try {
            headerParams.put("Signature",
                String.format("sig=:%s:",
                    sign(
                        String.format("\"@method\": %s%n\"@target-uri\": %s%n%s\"@signature-params\": %s",
                            method.toUpperCase(),
                            url,
                            contentDigest == null ? "" : "\"content-digest\": " + contentDigest + "\n",
                            signatureParameters
                        ),
                        privateKey
                    )
                )
            );
        } catch (Exception e) {
            throw new RuntimeException("Failed to sign request", e);
        }

        return headerParams;
    }

    /**
     * Remove the header and footer from the private key if generated via openssl, etc.
     *
     * @param rawKey The raw private key as a string
     * @return The prepared key without headers and footers
     */
    private String prepareKey(String rawKey) {
        String newKey = rawKey.replace("-----BEGIN PRIVATE KEY-----", "");
        newKey = newKey.replace("-----END PRIVATE KEY-----", "");
        return newKey.replaceAll("\\s+", "");
    }

    /**
     * Assemble the RFC 9421 signature parameters.
     *
     * @param contentDigest The digest of the content, if applicable
     * @return The signature parameters as a string
     */
    private String createSignatureParameters(String contentDigest) {
        return String.format("(\"@method\" \"@target-uri\"%s);created=%d;keyid=\"%s\";alg=\"%s\"",
            contentDigest == null ? "" : " \"content-digest\"",
            Instant.now().toEpochMilli() / 1000,
            clientId,
            SIGNATURE_ALGORITHM
        );
    }

    /**
     * Sign the request using SHA256withRSA.
     *
     * @param signatureBase The base string to sign
     * @param privateKey    The private key to use for signing
     * @return The Base64-encoded signature
     * @throws NoSuchAlgorithmException If the algorithm is not available
     * @throws InvalidKeyException      If the key is invalid
     * @throws SignatureException       If the signing process fails
     */
    private String sign(String signatureBase, PrivateKey privateKey)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        Signature signer = Signature.getInstance("SHA256withRSA");
        signer.initSign(privateKey);
        signer.update(signatureBase.getBytes());
        return Base64.getEncoder().encodeToString(signer.sign());
    }

    /**
     * Create and prepare the digest for the content-digest header.
     *
     * @param digestAlgorithm The algorithm to use for the digest
     * @param data            The data to digest
     * @return The formatted digest string
     * @throws NoSuchAlgorithmException If the algorithm is not available
     */
    private String createDigest(String digestAlgorithm, String data) throws NoSuchAlgorithmException {
        return String.format("%s=:%s:", digestAlgorithm, getDigest(digestAlgorithm, data));
    }

    /**
     * Generate the digest using the specified algorithm.
     *
     * @param algorithm The algorithm to use
     * @param data      The data to digest
     * @return The Base64-encoded digest
     * @throws NoSuchAlgorithmException If the algorithm is not available
     */
    private String getDigest(String algorithm, String data) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance(algorithm);
        byte[] hash = digest.digest(data.getBytes());
        return Base64.getEncoder().encodeToString(hash);
    }
}

❗️

Please request your clientID from your account manager

The clientId parameter within the signature headers is crucial for identifying the key used to sign the request. When you generate the signature headers, the clientId value is set to match with the public key registered with Layer1.

3. Use the Signed Headers in API Requests

Once the headers are generated, they should be attached to your HTTP request. These headers will allow the server to verify the signature using the corresponding public key that you registered Layer1. Simple put:

// Instantiate the http signer
HttpSigner signer = new HttpSigner(privateKey, clientId);

// Generate the signature headers
Map<String, String> headers = signer.buildHeaders(url, body, method);