Andrelia Hacks Stuff

T-Mobile: Signing Requests With Proof of Possession (PoP) Tokens

Introduction

In a typical authenticated API, leaked credentials allow an attacker to make API requests as the associated user for as long as the credentials are valid. With many REST APIs, these credentials come in the form of a bearer token, typically using JSON Web Tokens (JWTs).

The OAuth 2.0 bearer token specification, as defined in RFC6750, allows any party in possession of a bearer token to get access to the associated resources without demonstrating possession of a cryptographic key. Given the risk of exposure of these tokens, some organizations require additional security measures, such as request signing with a known keypair.

T-Mobile uses a method called proof of possession (PoP) which provides a mechanism to bind key material to OAuth tokens. The client can use a keypair to add a signature to the outgoing HTTP requests to the resource server. The resource server in turn can validate the signature and ensure that the key material provided by the sender is the same entity that requested the token in the first place (as opposed to someone who stole the token in transit or at rest). Further, each request is signed with its own PoP token, minimizing the risk of leaked access tokens being used across endpoints.

PoP Token Structure

The structure of the PoP token used by T-Mobile is:

Header: {alg, type}
Body: {
    iat: <epoch time>
    exp: <epoch time + 2 minutes>
    ehts: <authorization; content_type; uri; http-method; body> => All request headers, URI, HTTP method and body fields used to create hash
    edts: <Base64UrlSafeEncoding[SHA256(all ehts claim values as a concatenated string)]>
    jti: <unique identifier>
    v: "1"
}
Signature: <digitalSignature>

This token is then passed to T-Mobile API endpoints via the X-Authentication header, along with the access token in the Authorization header.

This is an example PoP token:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTExMTkwNTEsImV4cCI6MTcxMTExOTE3MSwiZWh0cyI6IkNvbnRlbnQtVHlwZTt1cmk7aHR0cC1tZXRob2QiLCJlZHRzIjoidHBBZG1QTWwyUV8yZlJVUjRPRWZsa25aUXR5VFloX3JLcVYzeXFiRFpBMCIsImp0aSI6ImZiMjc3ZGVmLTFkMDEtNDdiMy1hM2Q4LTk1NGJmY2MxNGY1OSIsInYiOjF9.HNfcPqaKufuj64eKDVgx-TahcM15PoCBJIE9RDJVirUMb4xOE0GhfxcXTmDyRVry-mlWp2czhS_cHQawXwvzdJ3SaJBQngWdQR8sApnaWcZcpjCjvysy5jxHEo3CSdKJdUNjA5Rzd7AboQaYpVvidwRUvhFdoUsOoXC3qVWiL1krqlwzHe-dvdKyRzdOSjmwuTHEGXmUS0WTm73449yzFoajgUJi4fh_Yi7Oz0Ac8o1jMh63GmtObA-D0uzQDMi-VVbrCsVhxDORL82xmuOoI1HyQZtZkjl69u1Ykgi-q5HjFNqm3YJC70QMfxcXNm9GFiC7X8X3T4tLr981_pDl5Q

When decoded, it contains the following data:

{
    "alg": "RS256",
    "typ": "JWT"
}
{
    "iat": 1711119051,
    "exp": 1711119171,
    "ehts": "Content-Type;uri;http-method",
    "edts": "tpAdmPMl2Q_2fRUR4OEflknZQtyTYh_rKqV3yqbDZA0",
    "jti": "fb277def-1d01-47b3-a3d8-954bfcc14f59",
    "v": 1
}
<signature>

Generating a PoP Token

The edts hash can be generated with the following data:

POST /oauth2/v2/tokens
Host core.saas.api.t-mobile.com
Content-Type: application/json
{"cnf":"-----BEGIN PUBLIC KEY-----...","client_id":"SIDWeb"}

Resulting in the following payload:

edtsString = "application/json/oauth2/v2/tokensPOST"
edts = base64.b64encode(SHA256.new(bytes(edtsString, "utf-8")).digest()).decode('utf-8').replace("=", "").replace("+", "-").replace("/", "_")
# tpAdmPMl2Q_2fRUR4OEflknZQtyTYh_rKqV3yqbDZA0

The backend API server can then take the public key provided in the POST body and validate the headers specified in the ehts claim.

Here is some sample Python code to generate the PoP token for a given request:

import base64
from datetime import datetime, timedelta, timezone
import uuid
from Crypto.Hash import SHA256
import jwt


def get_pop_token(priv_key, method, uri, headers=None, body=None):
    now = datetime.now(tz=timezone.utc)
    payload = {
        "iat": int(now.timestamp()),
        "exp": int((now + timedelta(seconds=120)).timestamp()),
    }

    data = {
        **headers,
        "uri": uri,
        "http-method": method
    }
    if body:
        data['body'] = body

    # Creating ehts string...
    ehts = []
    edtsString = ""
    for k, v in data.items():
        ehts.append(k)
        edtsString += str(v)
    payload['ehts'] = ';'.join(ehts)

    # Creating edts hash...
    hash = SHA256.new(bytes(edtsString, "utf-8"))
    edts = base64.b64encode(hash.digest()).decode('utf-8').replace("=", "").replace("+", "-").replace("/", "_")
    payload['edts'] = edts

    payload.update({
        "jti": str(uuid.uuid4()),
        "v": 1
    })

    return jwt.encode(payload, priv_key, algorithm="RS256")

Using PoP tokens

Once you have the ability to generate PoP tokens, you can request an access token:

def _access_token(self) -> str:
    endpoint = "/oauth2/v2/tokens"
    # These are the credentials associated with your T-Mobile app or user
    auth = HTTPBasicAuth(self.client_id, self.client_secret)
    headers = {"Content-Type": "application/json"}
    headers['X-Authorization'] = get_pop_token(self.priv_key, "POST", endpoint, headers=headers)
    r = requests.post(f"{self.base_url}{endpoint}", auth=auth, headers=headers)
    return r.json()['access_token']

And then finally you can put it all together to make an authenticated request to an API like this one for DevEdge:

def devices(self, iccid: str) -> Dict[Any, Any]:
    endpoint = f"/iot-connectivity/v1/devices/{iccid}"
    headers = {"Content-Type": "application/json"}
    headers['X-Authorization'] = get_pop_token(self.base.priv_key, "GET", endpoint, headers=headers)
    headers['Authorization'] = f'Bearer {self.base._access_token()}'
    r = requests.get(f"{self.base.base_url}{endpoint}", headers=headers)
    return r.json()