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
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
Securely save your private key.
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.
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:
Each line is concatenated with a newline character (\n).
You must respect the format (e.g., respect every character like ", @, or " " for instance).
@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