Signer is a simple token scheme based on xchacha20poly1305 to generate authenticatable and encrypted tokens issued to parties. Signer's wire format similar to "branca", except it utilizes url-safe base64 and omits branca's 32-bit binary time field.
The Token type is a byte slice implementing base64 MarshalText and UnmarshalText.
You have a server that wants to give clients a token. The server need to be able to verify that the token it issued came from the server (via the same key) and that this token was not modified by the client or some other party. The server also wants to keep the information inside the token private, and only accessible by the server or other parties in possession of the key. The authentication is symmetric. Only servers in possesion of the key can verify the token's authenticity.
You should not use Signer if the client (not just the server) needs to read and/or authenticate the token E.G., asymmetric authentication with RSA or an elliptic curve. This token is for servers that issue tokens, perhaps for sessions or other data.
For example: If you are using JWT with HMAC (and/or encryption), you can replace it with signer. If you are using JWT with RSA/EC, you can not.
Below is the Token's wire format:
version[1] nonce[24] ciphertext[...] tag[16] | base64
The first 1+24 bytes are the header, authenticated by the AEAD, but not encrypted.
The version is fixed to 0x41 (A)
The nonce is a randomly-generated 24-byte string
The rest is the output of the AEAD, the ciphertext and 16 byte tag.
The ciphertext is the encrypted msg.
The tag is a message authentication code (MAC) used to verify the integrity of the header and ciphertext
Finally, the vertical bar denotes the Token's intended string encoding is base64 (url-safe)
type Signer interface{
// Sign creates a token using the msg and nonce, if nonce is nil
// one is generated automatically using a CSPRNG (crypto/rand.Read)
Sign(msg []byte, nonce []byte) (Token, error)
// Verify authenticates the token and returns the decrypted msg
Verify(t Token) (msg []byte, err error)
}
// Configure
key := [32]byte{ /* random data */ }
s, _ := signer.New(key[:])
// Sign
tok, _ := s.Sign([]byte("hello world"), nil)
fmt.Println(tok)
// ul6mbjrzW_Y82_a8sQQRqlzFTPAcA65tn4xlWN3z3bpwIYZiW47JlyF34UwaUzize4yFfrN8Vzs
// Verify
p, err := s.Verify(tok)
if err != nil{
log.Fatalf("verify: %v", err)
}
Branca's uint32 time field isn't future-proof and ignorant of time predating 1970 (time is a signed value). Time in a token specification is scope creep. Add your own time in the msg to excersize full control, since its guaranteed to be authenitc.
Branca implementations seen in the wild don't return msg if it is authentic but expired, which is useless for practical deployments. With Signer, you have the ability to log authentic but expired tokens to debug misbehaving clients or bugs in clients software.
Branca utilizes a binary time field, one advantage of having a time field would be visibility in your server logs. For example, if logs contained a unix timestamp as part of the branca header, you could easily see in Splunk whether a certain timestamp was expired (assuming it wasn't modified by an adversary). However, Branca uses a binary timestamp, so the benefit of having a greppable/searchable value is quickly lost. You must decode the timestamp manually and in the correct big endian byte order before making assumptions about it even if you assume its authentic.
Branca uses base62, which is a clumsy, poorly-defined standard (branca test vectors could not be decoded by online base62 decoders). Prefer a standard encoding in a binary power of 2 that is easily accessible across languages.
Branca does not offer easily-available test vectors. Java implementations in the wild incorrectly implement AEADs that only authenticate the header and not the cipher text. We provide an interface that allows the user to pass in the nonce because in practice this is CRITICAL to reproducibly verifying test vectors in the implementation.
JWT is vulnerable to downgrade attacks, because it supports "none" as an encryption algorithm. Signer supports only one algorithm, so a downgrade attack is impossible by design. If chacha20poly1305 is broken in the distant future, you can use another type of token. It is not wise to rely on dynamic implementations based on token versions. Just tell the server what to expect.
JWT spec is complex and bloated. Signer is a bare-bones token that provides authentication and encryption only. It assumes the user can implement their own claims, authorization (not to be confused with authentication), and timeouts using the data inside the payload itself.
You want the client to be able to validate the contents of the token, using the servers public key. Signer does not support this usecase at the time of writing. However, it may be feasible to support this in another version if there is pressing need.
You don't want the contents of the token to be encrypted. Typically, encryption and authentication are erroneously confused for each other. Signer authenticates and encrypts the ciphertext. If you don't want encryption but want authentication, it might be good to consider other schemes (but this is not likely). In the case of tokens, you usually want the ability to verify the token. Keep in mind that a client who has a plaintext authenticated token has no way to verify its integrity unless they use asymmetric scheme such as RSA or Elliptic Curve, but these are orders of magnitude slower that symmetric encryption, so the performance benefit of dropping encryption and using only authentication in your scheme is lost. This might be a good usecase for a third version of signer that puts the entire token's content into the unencrypted part of the AEAD if there is pressing need. At this time, the idea of this usecase is unlikely.
This particular implementation of Signer allows the user to pass in a nonce. Nonces should be randomly-generated, as they serve to mitigate a class of attacks that can be used to extract functions of the plaintext from the ciphertext when the same nonce is used more than once for a different plaintext input. We assume the user is fluent enough to not do this, as generating a deterministic nonce with different input requires more effort on behalf of the user than calling the Sign method with a nil nonce. However, if you don't trust your users to do this, you may want to implement your own signer type that has a different method signature disallowing a custom nonce input. In practice, this makes it harder for understanding developers to impelement sanity checks by generating and verifying test vectors. In this package, the user can pass in a nonce because there are certain cases where a token needs to be regenerated from the same message and nonce pair determinstically.
The Token type uses URL-safe base64-encoding without padding characters. Below is the well-known character-set that comprises base64 url-safe encoding:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
This is the output of the zero vector, composed of a 32-byte key and nonce of all zero bits in binary and base64 url-safe format for your implementation in other languages:
name: "zero",
input: "",
key: [32]byte{},
nonce: [24]byte{},
binary: "A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\xac\x81ƕ\xb5;\xefw\n\xde5PU\xde",
text: "QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAisgcaVtc2-73cK3jVQVd4",