Tink JWT (HMAC signing) with Golang
[Tokens Home][Home]
So, what's a token? Well is basically a way to encapulate data in a well-defined format, and what 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 require the use of public key encryption. For this, we will generate a shared secret key, and then sign with an HMAC signature.
|
Outline
My Top 20 important things about JWTs:
- JWT is a JSON Web Token and pronounced "jot". JSON objects support human-readable text and are used in many applications, such as with NoSQL databases.
- You should not trust a JWT unless it is cryptographically signed.
- For authorization, a captured JWT can be captured and "played back" to provide a malicious entry to a system.
- JWTs should never be trusted before their issue date and their not-before date, and never trusted after their expiry.
- JWTs have been defined as an RFC standard with RFC7519.
- The format is URL friendly and is Base64URL encoded.
- A JWT token has three main parameters separated by a period ("."), and which are the header, the payload and the signature.
- The header is typically not encrypted and defines the signature algorithm ("alg") and the type ("typ").
- The payload is typically not encrypted and uses a Base64 format. The payload can typically be seen by anyone who captures it.
- "ey" is a typical field starting part of a parameter in the header and body of a token as '{"' encoded in Base64 is "ey==". You can tell if a token is not encrypted with an "ey" as the start of the header and body parameters.
- The registered claims of a token are iss (Issuer), sub (Subject), aud (Audience), iat (Issued At), exp (Expires), nbf (Not Before), and jti (JWT ID).
- The claim fields are not mandatory, and are just a starting point for defining claims.
- A claim is asserted about a subject, and where we have a claim name and a claim value in a JSON format.
- With an HMAC signature, the issuer and validator must share the same secret symmetric key.
- If you use HMAC to sign the tokens, a breached secret key will compromise the signing infrastructure.
- The two main public key signing methods are RSA and ECDSA.
- The time of a token is represented as the number of seconds from 1 January 1970 (UTC).
- Each day of a JWT token is represented by 86,400 seconds.
- An unsecured JWT does not have encryption or a signature. This is bad! It is represented in the header parameter with an "alg" of "none" and an empty string for the JWS Signature value.
- A JWT can be encrypted (but this is optional). For public key methods, we can use either RSA and AES, or we can use a wrapped key.
What's a token?
So, what's a token? Well is basically a way to encapulate data in a well-defined format, and what 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 require the use of public key encryption. For this, we will generate a shared secret key, and then sign with an HMAC signature.
JWT format
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 HS256 signatures and with an ID of "s5qe-Q" is:
{"alg":"HS256", "kid":"Pp8SRQ"}
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
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
example token is:
eyJhbGciOiJIUzI1NiIsICJraWQiOiJQcDhTUlEifQ.eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzk0MzcyLCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ.MMYznaGz9QlqY32soqzg389iPKEzhwls72cseVEhNps
We then have:
Header: eyJhbGciOiJIUzI1NiIsICJraWQiOiJQcDhTUlEifQ Payload: eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzk0MzcyLCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ Signature: MMYznaGz9QlqY32soqzg389iPKEzhwls72cseVEhNps
These are in Base64 format, and we can easily decode the header as:
{"alg":"HS256", "kid":"Pp8SRQ"}
and the payload as:
{"aud":"qwerty", "exp":1690794372, "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 HMAC-256, HMAC-384 or HMAC-512. In the following, we create a secret key (priv) and which will be used to sign the token:
mac, _ := jwt.NewMAC(key) token, _ := mac.ComputeMACAndEncode(rawJWT)
Next we can validate the token:
validator, _ := jwt.NewValidator(&jwt.ValidatorOpts{ExpectedAudience: &audience,ExpectedIssuer: &issurer}) verifiedJWT, _ := mac.VerifyMACAndDecode(token, validator)
The full code is:
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 () { key, _ := keyset.NewHandle(jwt.HS256Template()) 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: key,_ =keyset.NewHandle(jwt.HS256Template()) case 2: key,_ =keyset.NewHandle(jwt.HS384Template()) case 3: key,_ =keyset.NewHandle(jwt.HS512Template()) } mac, _ := jwt.NewMAC(key) rawJWT, _ := jwt.NewRawJWT(&jwt.RawJWTOptions{ Subject: &subject, Audience: &audience, Issuer: &issurer, ExpiresAt: &expiresAt, JWTID: &jwtid, }) token, _ := mac.ComputeMACAndEncode(rawJWT) validator, _ := jwt.NewValidator(&jwt.ValidatorOpts{ExpectedAudience: &audience,ExpectedIssuer: &issurer}) verifiedJWT, _ := mac.VerifyMACAndDecode(token, validator) id,_:=verifiedJWT.JWTID() sub,_:=verifiedJWT.Subject() aud,_:=verifiedJWT.Audiences() iss,_:=verifiedJWT.Issuer() at,_:=verifiedJWT.IssuedAt() ex,_:=verifiedJWT.ExpiresAt() fmt.Printf("Shared key:\t%s\n",key) 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(key, exportedPriv) fmt.Printf("Private key: %s\n\n", exportedPriv) }
A sample run gives:
Shared key: primary_key_id:1050612293 key_info:{type_url:"type.googleapis.com/google.crypto.tink.JwtHmacKey" status:ENABLED key_id:1050612293 output_prefix_type:TINK} Token: eyJhbGciOiJIUzI1NiIsICJraWQiOiJQcDhTUlEifQ.eyJhdWQiOiJxd2VydHkiLCAiZXhwIjoxNjkwNzk0MzcyLCAiaXNzIjoiQVNlY3VyaXR5U2l0ZSIsICJqdGkiOiIxMjM0NTYiLCAic3ViIjoiaGVsbG8ifQ.MMYznaGz9QlqY32soqzg389iPKEzhwls72cseVEhNps Subject: hello Audience: [qwerty] Issuer: ASecuritySite JWT ID: 123456 Issued at: 0001-01-01 00:00:00 +0000 UTC Expire at: 2023-07-31 09:06:12 +0000 GMT Additional key data Private key: .{primary_key_id:1050612293 key:{key_data:{type_url:"type.googleapis.com/google.crypto.tink.JwtHmacKey" value:"\x10\x01\x1a \xcbNLqq1i\xf8\xca\xcd\xc7\xd86\xa2\xc4\x00\xa8)\x9c_\xe9\x95h8%a\xe5.#36;\x8d\x12\xae\xc0" key_material_type:SYMMETRIC} status:ENABLED key_id:1050612293 output_prefix_type:TINK} .nil.}