Skip to main content

Verifying Response Signature

Truework API responses include a signature header that can be used to verify the authenticity of the API response.

Public Keys#

Truework's public key used to validate the signature will be published on this page.

KEY IDNOT VALID BEFORENOT VALID AFTERBase 64 Encoded Value
tw-2021-11-112021-11-11N/AAd14e4K+/6At51wUH2B3M4ONNlRmLiSpI4NUAl+IcyA=
tw-sandbox-2021-11-112021-11-11N/AyJBzPH0p+gdRuj4qnjU1+ob0AH52gHRDXxnDW1xGaYk=

X-Truework-Signature Parts#

The signature is included as a header named X-Truework-Signature and includes the information required to verify the signature. The signature has three parts: keyId, headers and signature. For example:

X-Truework-Signature: keyId="tw-2021-11-11", headers="date content-legth", signature="some_long_signature_b64_encoded"
Signature PartDetails
keyIdRefers to the key id of the public key that can be used to verify the signature
headersRefers to which response headers were used in the construction of the signature. Each header key is lower cased, seperated by a space character
signatureRefers to the actual signature that will be verified. Truework uses Ed25519 to generate the signature

Verifying the Signature#

Based on the headers part of the Signature header, we can construct the message that we will use to verify the signature. The message contains the following components, seperated by \n characters:

  1. Header key and value, in the format headerKey: headerValue
  2. Response body

For example, the message constructed for the API response:

Date:
Content-Length: 50
{"responseKey": "responseValue"}

Message constructed:

date: Fri, 12 Nov 2021 19:28:59 GMT\n
content-length: 50\n
{"responseKey": "responseValue"}

Using the constructed message, public key and signature, we can verify the response payload.

Code Example#

To verify the signature, we first parse the signature header to extract its parts:

signature = response.get_header("X-Truework-Signature")
signature_parts = {
i.split("=", 1)[0].strip(): i.split("=", 1)[1].strip('"')
for i in signature.split(",")
}
key_id = signature_parts["keyId"]
part_headers = signature_parts["headers"]
part_signature = signature_parts["signature"]

We then construct the message that we will use to verify the signature:

def get_headers_for_message(headers_part: str, response) -> dict:
headers = headers_part.split(" ")
return {i.lower(): response.get_header(i) for i in headers}
def get_message(headers: dict, body: str):
headers_to_sign = "\n".join([f"{key}: {value}" for key, value in headers.items()])
message = headers_to_sign + "\n" + body if headers_to_sign else body
return message
headers_for_message = get_headers_for_message(part_headers, response)
message = get_message(headers_for_message, response.body)

Finally, we verify the message using the constructed message, Truework's public key, and signature part:

from nacl.encoding import Base64Encoder
from nacl.exceptions import BadSignatureError
from nacl.signing import VerifyKey
def verify_signature(message: str, verify_key_bytes: bytes, signature: str):
verify_key = VerifyKey(verify_key_bytes)
signature_b64_bytes = signature.encode()
signature_bytes = Base64Encoder.decode(signature_b64_bytes)
return verify_key.verify(message.encode(), signature_bytes)
verify_key_b64_bytes = "tw_public_key_b64_encoded".encode()
verify_key_bytes = Base64Encoder.decode(verify_key_b64_bytes)
verify_signature(message, verify_key_bytes=verify_key_bytes, signature=part_signature)