Your server that receives requests for the configured URL(s) should accept traffic over HTTPS and use SSL/TLS. (Galileo supports TLS 1.2.) In addition to in-transit encryption, you must also perform validation on the messages received on these endpoints to ensure that they are indeed from Galileo.
Based on the Content-Type
of the request, which is configured according to your request, validation involves either a JWT (JSON web token) or a computed HMAC signature. Both involve use of a shared-secret key but have a number of differences that should be considered.
Similarities
- Both HMAC and JWT require that you configure a shared secret with Galileo.
- Both use the non-standard
Encryption-Type
header, which specifies the underlying algorithm used. - Both have a value of
galileo
for the standardUserID
header.
Differences
- HMAC is used for
Content-Type: application/x-www-form-urlencoded
requests, whereas JWT is used forContent-Type: application/json
. - Validation for requests that use HMAC is done using the non-standard
Signature
header, whereas those that use JWT use the standardAuthorization
header.
JWT Validation
The alternative approach (as shown in the Differences section) is to validate the JWT. For example, the request might look something like this:
POST /Authorization HTTP/1.1
Host: localhost:3150
X-Request-ID: 92c341e2-8b50-45bb-805c-8d0aa76124b8
Encryption-Type: JWT-HS256
Accept-Encoding: gzip, deflate
Content-Length: 380
Accept: */*
User-Agent: python-requests/2.9.1
Connection: keep-alive
Date: 20201110:163953UTC
User-ID: galileo
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnYWxpbGVvIiwiaWF0IjoxNjA1MDUxNTkzLCJleHAiOjE2MDUwNTE1OTZ9.jjHsPgAOZc13EGAGeqVHVHH0Nu_ajE5XpDcbDYmS1GI
{
"balance": "195.70",
"merch_name": "Target 2400 South 200 West",
"network": "M",
"timestamp": "20101108:201512UTC",
"cur_code": "840",
"tran_type": 5,
"prn": "155174792582",
"prod_id": 5042,
"amount": -8.95,
"blank": null,
"mcc": "4678",
"merch_num": "321654321654",
"prog_id": 511,
"type": "auth",
"id": "M.auth.2994428",
"tran_id": "2994428",
"merch_loc": "Salt Lake City, UT"
}
Again, the implementation is language/library dependent, but it might look something like this if you use Python:
import jwt
# Shared with Galileo
secret_key = 'wow_i_should_encrypt_this'
# This library wants us to ignore the 'Bearer' part, as we should
token = request.headers['Authorization'].split(' ')[1]
try:
jwt.decode(
token,
secret_key,
issuer=request.headers['User-ID'],
algorithms=['HS256'] # Won't change for JWT
)
except jwt.ExpiredSignatureError:
print('Oh no, the JWT is expired!')
raise
except jwt.DecodeError:
print("Oh no, this request probably isn't from Galileo!")
raise
HMAC validation
For example, a request that looks similar to the following:
POST /Authorization HTTP/1.1
Host: localhost:3150
X-Request-ID: 234dd0dc-8e44-4311-b334-961b1e058b72
Encryption-Type: HMAC-SHA256
Accept-Encoding: gzip, deflate
Content-Length: 424
Accept: */*
User-Agent: python-requests/2.9.1
Connection: keep-alive
Signature: b0+KPPtDyvuWdx55QJ+5YQq0K0UkXPurk9cRMHFXLSc=
Date: 20201110:161601UTC
User-ID: galileo
Content-Type: application/x-www-form-urlencoded
token_type=Apple+Inc.&settle_amt=20.00&auth_id=18056167&auth_network=M&local_surcharge_amt=2.00&merch_name=Arbys+1532&merchant=Arbys+1532+Rochester+MN&de39=00&billing_amt=30.00&original_auth_id=5&mcc=5814&settle_surcharge_amt=2.50&billing_curr_code=666&merch_num=945000153210000&auth_tran_type=5&settle_curr_code=555&local_curr_code=444&merch_loc=Rochester%2C+MN&card_not_present=N&amount=30.00&local_amt=10.00&sign_amount=-
- Create a dictionary/hash map using the header field of the request:
signature_data = {
'Content-Length': request['Content-Length'],
'Encryption-Type': request['Encryption-Type'],
'User-ID': request['User-ID'],
'Content-Type': request['Content-Type'],
'Date': request['date']
}
- Add the values from the form data in the request body to the hash map based on their key.
With a properly parsed body, you might be able to do something like this:
for field_key, field_value in request_body.items():
signature_data[field_key] = field_value
or more explicitly:
signature_data['token_type'] = 'Apple Inc.'
signature_data['settle_amt'] = '20.00'
...
for this simple case, assuming that you've parsed the form data into some sort of dictionary/hashmap,
- Sort the hashmap, by key, alphabetically. For example, your map contains something like this:
signature_data = {
'Content-Type': 'application/x-www-form-urlencoded',
'Encryption-Type': 'HMAC-SHA256',
'billing_amt': '30.00',
'token_type': 'Apple Inc.'
}
Notice that billing_amt
comes after Content-Type
, even though b
comes before c
in the English alphabet. In short, the set of upper-case letters comes before the set of lower-case letters.
- Compute the string:
signature_string = ''
for key in signature_data:
signature_string += key + '|' + base64encode(signature_data[key])
- Compute the
HMAC-SHA256
signature over this string, using the shared secret key. The specifics of this are language dependent based on the library used, among other considerations, but in Python it might look something like this:
import base64
import hashlib
import hmac
# Shared with Galileo
secret_key = 'wow_i_should_encrypt_this'
signature = base64.b64encode(
hmac.new(
secret_key,
signature_string,
hashlib.sha256
).digest(),
)
if signature != request.headers['Signature']:
print('Oh no!')
If you are having problems with this verification, ensure that all capitalization, whitespace, and other factors are maintained when computing the signature. Additionally, some HTTP libraries change the D
in User-ID
to look like User-Id
, for instance. The easiest way to validate these things is to obtain a raw version of the request and ensure that nothing has changed from the time it was received over the network socket to the time it made it into your code.