Using UUID as JWT Signing Key

This is not Go related but of a more general kind. However, I didn’t find any proper resource on this, hence I am asking here. I hope it is alright.

So, my question is, is it safe to use randomly generated UUID for JWT access token signing? I have very short duration JWT tokens (5 minutes) and UUID secret key is generated at server startup. Hence, with server restart, it will render existing tokens invalid, which is acceptable. However, I am wondering if there are any shortcomings with this approach, as opposed to using a static JWT with Environment Variable.

The short answer is: you’re probably fine but it’s also not ideal. First let’s talk about security. The answer to the question “are UUIDs secure?” is, of course, “it’s complicated and sometimes they are randomly generated per the spec but sometimes they aren’t”. Let’s take a look at a few important points from RFC 4122:

Identifier uniqueness considerations:
This document specifies three algorithms to generate UUIDs: the
first leverages the unique values of 802 MAC addresses to
guarantee uniqueness, the second uses pseudo-random number
generators, and the third uses cryptographic hashing and
application-provided text strings. As a result, the UUIDs
generated according to the mechanisms here will be unique from all
other UUIDs that have been or will be assigned.

OK - so it might be using cryptographic hashing or not. And in section 6:

  1. Security Considerations

Do not assume that UUIDs are hard to guess; they should not be used
as security capabilities (identifiers whose mere possession grants
access), for example. A predictable random number source will
exacerbate the situation.

In your case you have another layer of abstraction on top of the UUID since you’re only using it as a signing secret, not as a key. So, the attack vector for your app is:

  • Somebody would have to know you are using UUIDs as your signing secret in the first place. If this is a common pattern, it’s possible some bots out there are using this to attack APIs but honestly I doubt it.
  • The algorithm you are using to generate them would need to be one of the insecure (aka easily guessable) ones.
  • Based on the last time your server rebooted, they could then potentially guess your signing secret.

OK - so which type of UUIDs is Go generating? If you’re using google/uuid, you have the option to generate a random UUID:

// NewRandom returns a Random (Version 4) UUID.
//
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
// Uses the randomness pool if it was enabled with EnableRandPool.
//
// A note about uniqueness derived from the UUID Wikipedia entry:
//
//  Randomly generated UUIDs have 122 random bits.  One's annual risk of being
//  hit by a meteorite is estimated to be one chance in 17 billion, that
//  means the probability is about 0.00000000006 (6 × 10−11),
//  equivalent to the odds of creating a few tens of trillions of UUIDs in a
//  year and having one duplicate.
func NewRandom() (UUID, error) {
	if !poolEnabled {
		return NewRandomFromReader(rander)
	}
	return newRandomFromPool()
}

Note that that is using crypto/rand which is exactly what I would suggest you use. So - if you’re using that package and NewRandom, you’re (mostly) fine. Except for key size. A UUID is 128 bits (122 are random as 6 are reserved by the RFC). If you’re using, say, HMAC 256 (which is pretty common) as your signing algorithm you would want a 256 bit signing secret:

Hash-based Message Authentication Codes (HMACs) enable one to use a secret plus a cryptographic hash function to generate a MAC. This can be used to demonstrate that whoever generated the MAC was in possession of the MAC key. The algorithm for implementing and validating HMACs is provided in RFC 2104 [RFC2104].

A key of the same size as the hash output (for instance, 256 bits for “HS256”) or larger MUST be used with this algorithm. (This requirement is based on Section 5.3.4 (Security Effect of the HMAC Key) of NIST SP 800-117 [NIST.800-107], which states that the effective security strength is the minimum of the security strength of the key and two times the size of the internal hash value.)

Similarly if you switched to 512 or something in the future, you’d want a more flexible way of generating a larger random set of bits as your signing secret. In summary: I’d just switch to using crypto/rand to generate a secure random array of bytes at the length required by your signing method of choice.

BONUS: Why server state is bad

OK - so the final reason I would call your implementation “not ideal” is: it creates state in your API layer. Why might one avoid this? Because it means you can’t scale this horizontally.

A common pattern for deploying APIs is: a load balancer in front of multiple API servers. As long as they’re stateless, you can spin up any number of servers to handle that middle tier. You can also just deploy it to Google Cloud Run and have the instances managed for you.

Also - if you’re targeting a cloud for deploy a common pattern is some sort of secrets manager. They encrypt your sensitive data (passwords, signing secrets, private certs, etc.) and expose it to your instances via ENV vars or mounted drives. I don’t think you’re deploying to a cloud provider (yet) but by building your app this way you can future-proof it for when you do. :slight_smile:

For local dev mode I don’t like having to set ENV vars so I usually build my APIs in such a way that, if a config file is present it supersedes ENV vars.

2 Likes

Thanks for the detailed explanation. I have opted to use crypto/rand to generate 256 bits key instead. And regarding server state, you are absolutely correct. I am changing to first check if the environment variable is available; if not, then generate the key. That way it is best of both worlds I believe.