GoCardless Embed

API request signing

To further secure your API communication between GoCardless, you can enable API Request Signing. This section will walk you through how to:

  • Generate public and private keys and upload the public key to GoCardless

  • Build the signature base that will be used to generate the Gc-Signature header

  • Build and attach the Gc-Signature and Gc-Signature-Input headers and send them with you API request

Generate your key pair

  1. We use ECDSA keys to be used in API request signing. You must generate an ES512 key pair using the P-521 elliptic curve without using a passphrase. Use the following commands in a terminal to generate your key pair

    openssl ecparam -name secp521r1 -genkey -noout -out ec-secp521r1-priv-key.pem openssl ec -in ec-secp521r1-priv-key.pem -pubout > ec-secp521r1-pub-key.pem
  2. Securely save your private key.

  3. Upload your public key to GoCardless via the dashboard. 

    • Go to Settings -> Configurations

    • Under API Request Keys, add a new public key.

      cat ec-secp521r1-pub-key.pem # Copy this output and paste into the form. It should look something like: -----BEGIN PUBLIC KEY----- MIGbMBAGByqG..................... ................................. ................ -----END PUBLIC KEY-----
    • When you’ve added a new public key, a corresponding request_signing_key ID (e.g RSK00123456789300123456789300) is generated. You will need to use this for building the request signature.

Signing the API request

The following section describes how to sign your API Requests.

We follow the RFC-9421 specification for HTTP Message Signatures. For more details, refer to this document.

  1. Create the signature base. This is a raw string containing all the components covered by the signature. It must be in the format as shown in the example below.

    "@method": POST "@authority": api.gocardless.com "@request-target": /path?param=value "content-digest": sha256=:9AmU2hRsZnqEo2HiNTLacLN0fOs8YmDiuX4WYeYWvh0=: "content-type": application/json "content-length": 18 "@signature-params": ("@method" "@authority" "@request-target" "content-digest" "content-type" "content-length");keyid="your-public-key-identifier";created=1675688690;nonce="8IBTHwOdqNKAWeKl7plt8g=="

    Requirements:

    1. Each line is concatenated with a newline character (\n)

    2. You must respect the format (e.g., respect every character like ", @, or " " for instance).

    3. @method @authority and @request-target are required components in building the signature base

    You can add any other signed headers you want to include into the signature base. They must appear in the same order as listed in @signature-params. For any additional signed headers you must pass them as HTTP headers in the request itself. The following table describes how this message is built:

Key

Value

“@method”

The HTTP method in uppercase, e.g. GET or POST.

“@authority”

The target URI of the HTTP request message, usually conveyed using the Host header field

(HTTP host e.g. api.gocardless.com)

“@request-target”

The full path of the request including any query parameters

Note: The query parameters must be ordered deterministically by sorting keys or other standards, ensuring consistent request target

“content-digest”

Only needs to be included if the API request has a body (e.g. POST/PUT).

The SHA 256 and base64 encoded hash of the body, wrapped as follows between two colon (:) characters e.g. sha256=:CONTENT_DIGEST:

Note: The payload must be serialised deterministically, ensuring consistent JSON string representation by sorting keys and maintaining other serialisation standards to guarantee that the same request body produces the same content digest

“content-type”

Only needs to be included if the API request has a body.

Specify the type of content being sent to the server e.g. application/json

“content-length”

Only needs to be included if the API request has a body.

Indicates the size of the message body, in bytes.

“@signature-params”

The following signature parameters should be passed as follows and separated by semicolon:

(<signature components>);<signature param1>;<signature param2>

Signature Components In brackets - the components of the request that are included in the signature process. This would be "@method" "@authority" "@request-target", followed by any additional request headers, each separated by a space “ “. They should appear in the order as you used to build the rest of the signature base.

Signature Parameters keyid: the identifier of your public request signing key.

created: a Unix timestamp (e.g. 1675688690) in UTC

nonce: A random unique value generated for this signature as a String value. This must be a random base64-encoded string of at least 128 bits of data from a cryptographically secure random number generator.

More details on building the signature base can be found in section 2.5 of RFC-9421.

2. Sign the signature base with you private key

private_key = OpenSSL::PKey::EC.new(your_private_key) signature = private_key.sign(OpenSSL::Digest.new("SHA512"), signature_base)

3. Encode the signature in base64

encoded_signature = Base64.strict_encode64(signature)

4. Set the `Gc-Signature` and `Gc-Signature-Input` HTTP headers.

Requirements

  • `Gc-Signature` is the base64 encoded signature. 

    • It must be in the format Gc-Signature: sig-1=:<SIGNATURE>: where sig-1 is the label used to identify this signature and the signature is wrapped in between colon (:) characters

  • `Gc-Signature-Input` is the same string as the @signature-params used in building the signature base.

    • It must be in the format sig-1=<SIGNATURE_INPUT> where sig-1 is the label used to identify this signature

Gc-Signature-Input: sig-1=("@method" "@authority" "@request-target" "content-digest" "content-type" "content-length");keyid="your-public-key-identifier";created=1675688690;nonce="8IBTHwOdqNKAWeKl7plt8g==" Gc-Signature: sig-1=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQ\ EdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT\ 8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SA\ RYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd\ 4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoK\ UqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:

5. Send your request with the attached headers.

  • Remember, if making a request with a payload, the payload sent must be of the same structure as you used previously to generate the content-digest to ensure they match during decryption. (See table above on "content-digest" for more details")

Code Example

Example methods for building the signature headers

module SignatureHelper ### Build Signature Base ### def self.signature_base(http_method, host, request_uri, content_digest, content_type, content_length, key_id, created, nonce) signature_base = <<~SIGBASE "@method": #{http_method} "@authority": #{host} "@request-target": #{request_uri} "content-digest": sha256=:#{content_digest}: "content-type": #{content_type} "content-length": #{content_length} "@signature-params": #{signature_params(key_id, created, nonce)} SIGBASE signature_base.strip end ### Build Signature Params ### def self.signature_params(key_id, created, nonce) '("@method" "@authority" "@request-target" "content-digest" "content-type" "content-length");keyid="%s";created=%d;nonce="%s"' % [key_id, created, nonce] end ### Sign and return base64 encoded signature for `Gc-Signature` header ### def self.signature_header(private_key_pem, signature_base) private_key = OpenSSL::PKey::EC.new(private_key_pem) signature = private_key.sign(OpenSSL::Digest.new("SHA512"), signature_base) signature = Base64.strict_encode64(signature) "sig-1=:#{signature}:" end ### Return value for `Gc-Signature-Input` header ### def self.signature_input_header(key_id, created, nonce) "sig-1=#{signature_params(key_id, created, nonce)}" end ### Sort query parameters to be used in request URL and @request-target ### def self.order_query_params(original_url) return original_url if original_url.nil? || original_url.empty? uri = URI.parse(original_url) return original_url unless uri.query query_params = URI.decode_www_form(uri.query).sort uri.query = URI.encode_www_form(query_params) uri.to_s end ### Sort request body to be used in request and Content-Digest ### def self.order_request_body(request_body) json_string = JSON.generate(sort_hash(request_body)) json_string end def self.sort_hash(hash) # Recursively sort the hash and its nested hashes hash.each_with_object({}) do |(key, value), sorted_hash| sorted_key = key.to_s # Convert key to string sorted_value = value.is_a?(Hash) ? sort_hash(value) : value sorted_hash[sorted_key] = sorted_value end.sort.to_h # Sort the hash by keys and convert back to a hash end ### Calculate Content-Digest ### def self.content_digest(request_body) digest = OpenSSL::Digest.new("SHA256").digest(request_body) Base64.strict_encode64(digest) end end

You can then use this code to generate the HTTP Signature headers and attach them to your request:

def make_request(request_body, request_uri, private_key) sorted_request_body = SignatureHelper.order_request_body(request_body) content_digest = SignatureHelper.content_digest(sorted_request_body) content_length = sorted_request_body.bytesize content_type = "application/json" sorted_request_uri = SignatureHelper.order_query_params(request_uri) created = Time.now.to_i nonce = SecureRandom.alphanumeric(16) key_id = "YOUR_REQUEST_SIGNING_KEY_ID" signature_base = SignatureHelper.signature_base("POST", "api-sandbox.gocardless.com", sorted_request_uri, content_digest, content_type, content_length, key_id, created, nonce) signature_header = SignatureHelper.signature_header(private_key, signature_base) signature_input_header = SignatureHelper.signature_input_header(key_id, created, nonce) # Make the request attaching the required headers end

Note (WIP) GoCardless Client Libraries will be updated to include helper methods similar to the above to aid with generating request signatures

API Response

API requests that are correctly signed will be processed normally. If it is not correctly signed, the API will return 401 Unauthorized.

Test your request signing

To test if you are correctly building the request signatures, you can make a POST request to the /test-signature endpoint which will validate the Gc-Signature and Gc-Signature-Input headers. If the signature is valid, the API will return a 204 No Content. The API will do no further validations on any request body that is also passed.

Example:

curl --silent \ -H "Content-Type: application/json" \ -H "Content-Digest: sha256=:dg0ak4ae6PgXhyxkn0FYx0th5QxzaDabkM2wBtufB2g=:" \ -H "Content-Length: 16" \ -H "Gocardless-Version: 2015-07-06" \ -H "Authorization: Bearer ${API_ACCESS_TOKEN}" \ -H "Gc-Signature: sig-1=:${SIGNATURE}:" \ -H "Gc-Signature-Input: sig-1=("@method" "@authority" "@request-target" "content-digest" "content-type" "content-length");keyid="${KEY_ID}";created=${created};nonce="${nonce}"" \ "https://api.gocardless.com/test_signature" \ -d @- << EOF { "foo": "bar" } EOF

Need help?