HMAC

JWT HTTP HMAC Spec

Features:

  • Signs request method, path, query, preferred headers and request body
  • Optionally supports token expiration and "Not Before" times (helps to prevent Replay attacks)
  • Signs response status code, preferred headers and response body
  • Reuses nonce from the request to ensure that response belongs to the request
  • Supports HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512 singing algorithms

Overview of Request Header and Signature

The pseudocode below illustrates construction of the HTTP signature header:

RequestNonce = HexV4OfRandomUUID()

SignedHeaders: Array(n) = [ 
	HTTP-Header-Value, for each RequestHeadersToSign. null if undefined.
]

DataToHash: Array(6) = [
	RequestNonce
	Uppercase( HTTPVerb )
	PathWithQuery
	SignedHeaders
	RequestBody or null
]

HashedData = Base64( SHA256( JSONStringify( DataToHash ) ) )

Payload = {
	jti = RequestNonce
	sub = Subject
	headers = RequestHeadersToSign
	hash = HashedData
}

Token = JWTSign( Payload, Secret )

Singature = "jwt-http-hmac/1.0" + " " + Token

  • RequestNonce - UUID v4
  • RequestHeadersToSign - an array of header names client wishes to sign
  • HTTPVerb - GET, POST, PUT etc.
  • PathWithQuery - '/path/to/the/endpoint?param=one'
  • SignedHeaders - an array of values of headers defined in RequestHeadersToSign array in the exact order
  • RequestBody - request body payload. null for requests with no request payload (ie GET).

Example

Predefined variables

For the sake of this example these variables will be predefined so that you can verify the correct implementation.

Nonce

62934be2-adab-4964-9d18-e4360fcb29de

Secret

WzQXBajnHpi1Q2li+gf/z4dKBGLyRd1/WpOWZIwGQydh7x/DFv4Rkt/uMe8USQIVSH9FD30SlqEWqUx99aTI4Q==

Signing process

In this example we will sign the GET request along with the Test header value.

GET /users?limit=5 HTTP/1.1
Host: splyt.com
Content-Type: text/plain
Test: one
Accept: */*

First thing to do would be to flatten the names of the headers we would like to sign into a one dimentional ordered array SignedHeaders:

["one"]

Then the value of DataToHash would be equal to:

[
  "62934be2-adab-4964-9d18-e4360fcb29de",
  "splyt.com",
  "GET",
  "/users?limit=5",
  [ "one" ],
  null
]

After Base64( SHA256( JSONStringify( DataToHash ) ) ) we shouldget back value m1fDgPOWWXXJtvgqjn3M1fonUvVQnuNtMOCexeCwXOI=

Having that we can build a JWT payload.

{
  headers: [ 'test' ],
  hash: 'm1fDgPOWWXXJtvgqjn3M1fonUvVQnuNtMOCexeCwXOI=',
  sub: 'urn:partner',
  jti: '62934be2-adab-4964-9d18-e4360fcb29de'
}

Using our predefined secret from this example we can sign the JWT payload using HS256 algorithm. We should get the following:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoZWFkZXJzIjpbInRlc3QiXSwiaGFzaCI6Im0xZkRnUE9XV1hYSnR2Z3FqbjNNMWZvblV2VlFudU50TU9DZXhlQ3dYT0k9Iiwic3ViIjoidXJuOnBhcnRuZXIiLCJqdGkiOiI2MjkzNGJlMi1hZGFiLTQ5NjQtOWQxOC1lNDM2MGZjYjI5ZGUifQ.7WP66f3TneZfV_wY2vu6I0NweKE_fFCn9B8KbugP6No

Now the original request should be transformed to include signature header as following:

GET /users?limit=5 HTTP/1.1
Host: splyt.com
Content-Type: text/plain
Test: one
Accept: */*
X-JWT-Signature: jwt-http-hmac/1.0 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoZWFkZXJzIjpbInRlc3QiXSwiaGFzaCI6Im0xZkRnUE9XV1hYSnR2Z3FqbjNNMWZvblV2VlFudU50TU9DZXhlQ3dYT0k9Iiwic3ViIjoidXJuOnBhcnRuZXIiLCJqdGkiOiI2MjkzNGJlMi1hZGFiLTQ5NjQtOWQxOC1lNDM2MGZjYjI5ZGUifQ.7WP66f3TneZfV_wY2vu6I0NweKE_fFCn9B8KbugP6No

Overview of Request Verification

The pseudocode below illustrates verification algorithm of the signed HTTP request:

Token = extracted token from the request header

Payload = JWTVerifyAndDecode( Token, Secret )

SignedHeaders: Array(n) = [ 
	HTTP-Header-Value, for each Payload.headers. null if undefined.
]

DataToSign: Array(6) = [
	Payload.jti # aka nonce
	Uppercase( HTTPVerb )
	PathWithQuery
	SignedHeaders
	RequestBody or null
]

HashedData = Base64( SHA256( JSONStringify( DataToSign ) ) )

SignatureValid = if HashedData == Payload.hash

Overview of Response Header and Signature

RequestNonce = extracted from request signature

SignedHeaders: Array(n) = [ 
	HTTP-Header-Value, for each ResponseHeadersToSign. null if undefined.
]

DataToSign: Array(4) = [
	RequestNonce
	HTTPStatusCode
	SignedHeaders
	ResponseBody or null
]

HashedData = Base64( SHA256( JSONStringify( DataToSign ) ) )

Payload = {
	jti = RequestNonce
	headers = ResponseHeadersToSign
	hash = HashedData
}

Signature = JWTSign( Payload, Secret )

Overview of Response Verification

Token = extracted token from the response header

Payload = JWTVerifyAndDecode( Token, Secret )

SignedHeaders: Array(n) = [ 
	HTTP-Header-Value, for each Payload.headers in exact order. null if undefined.
]

DataToSign: Array(4) = [
	data.jti
	HTTPStatusCode
	SignedHeaders
	ResponseBody or null
]

HashedData = Base64( SHA256( JSONStringify( DataToSign ) ) )

SignatureValid = if HashedData == Payload.hash AND data.jti == RequestNonce

Glossary

  • Token - contents of a X-JWT-Signature header of the response.
  • Secret - Secret, Private or Public key