The Core of your Trust Infrastructure … Access Tokens

We are increasenly moving to trust infrastructures that are based on claims to the usage of services, and these are granted with signed…

The Core of your Trust Infrastructure … Access Tokens

We are increasingly moving to trust infrastructures that are based on claims to the usage of services, and these are granted with signed tokens that provide access. The trust level is defined by the entity that signed the token.

For a JWT token, we can either sign with a MAC (Message Authentication Code) or use public key encryption (EC or RSA). As an introduction, here are the basic methods:

What’s a token?

Do you know the difference between a MAC (Message Authentication Code) and public key signing? Well a MAC uses a secret key that is shared between Bob and Alice, while public key signing involves Bob signing with his private key, and Alice proving the signature with his public key. Overall, public key signing of tokens is generally more secure and trust, but has the overhead of needing to distribute the public key. Many organisations use a MAC approach to signing tokens, but if Eve discovers the shared secret key, there can be a significant loss of trust.

So, what’s a token? Well, it is basically a way to encapsulate data in a well-defined format that has a signature from the issuer. For this, we either sign using HMAC (HMAC-256, HMAC-384 or HMAC-512), RSA signing or ECC signing. The HMAC method requires the use of a secret symmetric key, whilst RSA and ECC use public key encryption. The problem with HMAC is that if someone discovers the secret key, they could sign valid tokens. For enhanced security, we typically use public key encryption, where we sign with a private key and then validate with the public key.

In this case, we will use Google Tink to create JWTs (JSON Web Tokens) and which are signed with elliptic curve cryptography. For this, we will use either NIST P-256 (ES256), NIST P-384 (ES384) or NIST P512 (ES512) for the curves. Overall, we do not generally encrypt the payload in JWT, so it can typically be viewed if the token is captured.

JWT format

A JWT token splits into three files: header, payload and signature (Figure 1).

Figure 1: JWT format

The header parameter

The header contains the formation required to interpret the rest of the token. Typical fields are “alg” and “kid”, and these represent the algorithm you use (such as “ES256”) and the ID, representively. The default type (“type”) will be “JWT”. Other possible fields include “jwk” (JSON Web key), “cty” (Content type), and “x5u” (X.509 URL). An example header for a token that uses ES384 signatures and with an ID of “s5qe-Q” is:

{"alg":"ES384", "kid":"s5qe-Q"}

The payload parameter

The payload is defined in JSON format with a key-pair setting. For a token, we have standard claim fields of iss (Issuer), sub (Subject), aud (Audience), iat (Issued At), exp (Expires At), nbf (Not Before), and jti (JWT ID). The claim fields are not mandatory and are just a starting point — and where a developer can add any field that they want. An example field is:

{"aud":"qwerty", "exp":1690754794, "iss":"ASecuritySite", 
"jti":"123456", "sub":"hello"}

The time is defined in the number of seconds past 1 January 1970 UTC. In this case, 1690754794 represents Sunday 30 Jun 22:06:34:

The token signing parameter

There are two ways to sign a token: with an HMAC signature or with a public key signature. With HMAC, we create a shared symmetric key between the issuer and the validator. For public key encryption, we use either RSA or ECDSA. For these, we create a signature by signing the data in the token with the private key of the creator of the token, and then the client can prove this with the associated public key. For public key signing, the main signing methods are:

  • ES256. ECDSA using NIST P256 with SHA-256.
  • ES384. ECDSA using NIST P384 with SHA-384.
  • ES512. ECDSA using NIST P512 with SHA-512.
  • RS256. RSASSA-PKCS1-v1_5 with the SHA-256 hash.

and for HMAC:

  • HS256. HMAC with SHA-256.
  • HS384. HMAC with SHA-384.
  • HS512. HMAC with SHA-512.

In public key signing, we have a key pair to sign the token:

And with HMAC, we share a secret signing key:

Encrypting the payload

A JWT can be encrypted, but this is optional. For public key methods, we can use either RSA and AES or a wrapped AES key. An “alg” method of “RSA1_5” will use 2,048-bit RSA encryption with RSAES-PKCS1-v1_5, “A128KW” will use 128-bit Key Wapping and “A256KW” uses 256-bit Key Wapping. With key wrapping, the private key is encrypted with a secret key. Both the issuer and verifier will know this secrete key.

For symmetric key methods, we can use “A128CBC-HS256” (AES-CBC) and “A256CBC-HS512” (HMAC SHA-2). It is possible to also use ECDH-ES (Elliptic Curve Static) for key exchange methods

An example token

An example token is:

eyJhbGciOiJFUzI1NiIsICJraWQiOiJ3WHd6dVEifQ.eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzU0Nzk0LCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ.cAXunJHLRrqFfJStJTFlwkUTze6K8EpwOui9abDeiSBcR5WeOEpXCSUQBnS_VdVnLsmVV2AWUX0kOTqIWERcMQ

We then have:

  • Header: eyJhbGciOiJFUzI1NiIsICJraWQiOiJ3WHd6dVEifQ
  • Payload: eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzU0Nzk0LCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ
  • Signature: cAXunJHLRrqFfJStJTFlwkUTze6K8EpwOui9abDeiSBcR5WeOEpXCSUQBnS_VdVnLsmVV2AWUX0kOTqIWERcMQ

These are in Base64 format, and we can easily decode the header as:

{"alg":"ES256", "kid":"wXwzuQ"}

and the payload as:

{"aud":"qwerty", "exp":1690754794, "iss":"ASecuritySite", 
"jti":"123456", "sub":"hello"}

The signature value will be in a byte array format.

Sample code

With Google Tink, we can create a token with the fields using:

expiresAt := time.Now().Add(time.Hour)
subject:= "CSN09112"
audience := "Sales"
issurer := "ASecuritySite"
jwtid := "123456"

rawJWT, _ := jwt.NewRawJWT(&jwt.RawJWTOptions{
Subject: &subject,
Audience: &audience,
Issuer: &issurer,
ExpiresAt: &expiresAt,
JWTID: &jwtid,
})

Next, we will generate an ECC private key using either NIST P256, NIST P-384 or NIST P-512. In the following, we create a private key (priv) and which will be used to sign the token:

priv,_ =keyset.NewHandle(jwt.ES256Template()
signer, _ := jwt.NewSigner(priv)
token, _ := signer.SignAndEncode(rawJWT)

We can then create the public key from the private key, and validate the token with this key:

pub, _:= priv.Public()
verifier, _ := jwt.NewVerifier(pub)

The full code is [here]:

package main
import (
"fmt"
"time"
"os"
"strconv"
"github.com/google/tink/go/jwt"
"github.com/google/tink/go/keyset"
"github.com/google/tink/go/insecurecleartextkeyset"
)
func main () {
priv, _ := keyset.NewHandle(jwt.ES256Template())
expiresAt := time.Now().Add(time.Hour)
subject:= "CSN09112"
audience := "Sales"
issurer := "ASecuritySite"
jwtid := "123456"
t:=0
argCount := len(os.Args[1:])
if (argCount>0) {subject= string(os.Args[1])}
if (argCount>1) {audience= string(os.Args[2])}
if (argCount>2) {issurer= string(os.Args[3])}
if (argCount>3) {jwtid= string(os.Args[4])}
if (argCount>4) {t,_ = strconv.Atoi(os.Args[5])}
switch t {
case 1: priv,_ =keyset.NewHandle(jwt.ES256Template())
case 2: priv,_ =keyset.NewHandle(jwt.ES384Template())
case 3: priv,_ =keyset.NewHandle(jwt.ES512Template())
}
pub, _:= priv.Public()
rawJWT, _ := jwt.NewRawJWT(&jwt.RawJWTOptions{
Subject: &subject,
Audience: &audience,
Issuer: &issurer,
ExpiresAt: &expiresAt,
JWTID: &jwtid,
})
signer, _ := jwt.NewSigner(priv)
token, _ := signer.SignAndEncode(rawJWT)
verifier, _ := jwt.NewVerifier(pub)
validator, _ := jwt.NewValidator(&jwt.ValidatorOpts{ExpectedAudience: &audience,ExpectedIssuer: &issurer})
verifiedJWT, _:= verifier.VerifyAndDecode(token, validator)
id,_:=verifiedJWT.JWTID()
sub,_:=verifiedJWT.Subject()
aud,_:=verifiedJWT.Audiences()
iss,_:=verifiedJWT.Issuer()
at,_:=verifiedJWT.IssuedAt()
ex,_:=verifiedJWT.ExpiresAt()
fmt.Printf("Public key:\t%s\n",priv)
fmt.Printf("Public key:\t%s\n\n",pub)
fmt.Printf("Token:\t%s\n\n",token)
fmt.Printf("Subject:\t%s\n",sub)
fmt.Printf("Audience:\t%s\n",aud)
fmt.Printf("Issuer:\t\t%s\n",iss)
fmt.Printf("JWT ID:\t\t%s\n",id)
fmt.Printf("Issued at:\t%s\n",at)
fmt.Printf("Expire at:\t%s\n",ex)
fmt.Printf("\n\nAdditional key data\n")
exportedPriv := &keyset.MemReaderWriter{}
insecurecleartextkeyset.Write(priv, exportedPriv)
fmt.Printf("Private key: %s\n\n", exportedPriv)
exportedPub := &keyset.MemReaderWriter{}
insecurecleartextkeyset.Write(pub, exportedPub)
fmt.Printf("Public key: %s\n\n", exportedPub)
}

A sample run proves the process [here]:

Public key:	primary_key_id:1926408156  key_info:{type_url:"type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey"  status:ENABLED  key_id:1926408156  output_prefix_type:TINK}
Public key: primary_key_id:1926408156 key_info:{type_url:"type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey" status:ENABLED key_id:1926408156 output_prefix_type:TINK}
Token: eyJhbGciOiJFUzI1NiIsICJraWQiOiJjdEtuM0EifQ.eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzUxNTI0LCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ.qfui2u9hBpEgiQQeeWNJtSanyl4rbYkViIZJxVmBvCsP72ovcT20qC35YbQOh7Q8cCqA37Fk8OXWSQ-geg6E-Q
Subject: hello
Audience: [qwerty]
Issuer: ASecuritySite
JWT ID: 123456
Issued at: 0001-01-01 00:00:00 +0000 UTC
Expire at: 2023-07-30 21:12:04 +0000 GMT
Additional key data
Private key: .{primary_key_id:1926408156 key:{key_data:{type_url:"type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey" value:"\x12F\x10\x01\x1a \xcdPtI\x03)\xb0\xf7H9'\x1e\x94t\xaaa\x99\xf8ڍv\xcf\xd6|\x1a\x1aV6H!\xda\x00\" \xc2ϥ\xfaD\x16\xb2\xfa\xd7\x00\xfe\xba\xe4\xf3\xed%\x03\x9a^\x1d\x9f\x93_\xf3\x1f\xd9W\x90\x8aâX\x1a p\xf7,_}\x13\xff\x84\x9c\xc6j\xdaͯ\xc7\x1b.\xb2|\x19؎\xfb\xa9j\x05\xb3NF\xc4\x7f\xcc" key_material_type:ASYMMETRIC_PRIVATE} status:ENABLED key_id:1926408156 output_prefix_type:TINK} .nil.}
Public key: .{primary_key_id:1926408156 key:{key_data:{type_url:"type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey" value:"\x10\x01\x1a \xcdPtI\x03)\xb0\xf7H9'\x1e\x94t\xaaa\x99\xf8ڍv\xcf\xd6|\x1a\x1aV6H!\xda\x00\" \xc2ϥ\xfaD\x16\xb2\xfa\xd7\x00\xfe\xba\xe4\xf3\xed%\x03\x9a^\x1d\x9f\x93_\xf3\x1f\xd9W\x90\x8aâX" key_material_type:ASYMMETRIC_PUBLIC} status:ENABLED key_id:1926408156 output_prefix_type:TINK} .nil.}

Conclusions

There are many risks in using JWTs, especially in capturing a token and playing it back. The expiry date should thus be set so that it would limit the impact of any malicious use.

Using public key encryption to sign JWTs is a good method, as the authenticity of the token can be proven with the associated public key. With an HMAC method, we need to share a secret key, which could cause a data breach.

And, finally, which signature method should you pick? Find out here:

https://billatnapier.medium.com/hmac-or-public-key-signing-of-jwts-64084aff10ef