Related blog: https://medium.com/asecuritysite-when-bob-met-alice/tokens-jwt-and-google-tink-c6b915d387e8
And: https://billatnapier.medium.com/hmac-or-public-key-signing-of-jwts-64084aff10ef
Introduction
My Top 20 important things about JWTs:
And a debate I’ve had with many development teams:
What’s a token?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 formatA JWT token splits into three files: header, payload and signature (Figure 1).
Figure 1: JWT format The header parameterThe 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 parameterThe 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 parameterThere 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:
and for HMAC:
In public key signing, we have a key pair to sign the token:
And with HMAC, we share a secret signing key:
Encrypting the payloadA 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 tokenAn example token is:
eyJhbGciOiJFUzI1NiIsICJraWQiOiJ3WHd6dVEifQ.eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzU0Nzk0LCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ.cAXunJHLRrqFfJStJTFlwkUTze6K8EpwOui9abDeiSBcR5WeOEpXCSUQBnS_VdVnLsmVV2AWUX0kOTqIWERcMQWe then have:
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 codeWith 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 mainimport ( "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-QSubject: helloAudience: [qwerty]Issuer: ASecuritySiteJWT ID: 123456Issued at: 0001-01-01 00:00:00 +0000 UTCExpire at: 2023-07-30 21:12:04 +0000 GMTAdditional key dataPrivate 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.} ConclusionsThere 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: