HTTP Response Signatures

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=

Signature

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-length", 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, separated 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, separated 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:

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

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

1def get_headers_for_message(headers_part: str, response) -> dict:
2 headers = headers_part.split(" ")
3 return {i.lower(): response.get_header(i) for i in headers}
4
5def get_message(headers: dict, body: str):
6 headers_to_sign = "\n".join([f"{key}: {value}" for key, value in headers.items()])
7 message = headers_to_sign + "\n" + body if headers_to_sign else body
8 return message
9
10headers_for_message = get_headers_for_message(part_headers, response)
11message = get_message(headers_for_message, response.body)

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

1from nacl.encoding import Base64Encoder
2from nacl.exceptions import BadSignatureError
3from nacl.signing import VerifyKey
4
5def verify_signature(message: str, verify_key_bytes: bytes, signature: str):
6 verify_key = VerifyKey(verify_key_bytes)
7 signature_b64_bytes = signature.encode()
8 signature_bytes = Base64Encoder.decode(signature_b64_bytes)
9 return verify_key.verify(message.encode(), signature_bytes)
10
11verify_key_b64_bytes = "tw_public_key_b64_encoded".encode()
12verify_key_bytes = Base64Encoder.decode(verify_key_b64_bytes)
13verify_signature(message, verify_key_bytes=verify_key_bytes, signature=part_signature)