What is PASETO ?
PASETO is a new specification (still in draft) describing a way of creating secure and stateless tokens. It's an acronyme for Platform Agnostic Security tokens.
It's design for solving security problems with the usage of the JOSE family specification (JWS, JWE, JKW, JWA and JWT). As explain on the RFC:
The key difference bewtween PASETO and the JOSE family of standards is that JOSE allows implementors and users to mix and match their own choice of cryptographic algorithms (specified by the "alg" header ing JWT), while PASETO has clearly defined protocol versions to prevent unsafe configurations from being selected.
If you want to learn more about PASETO, you can read this article written by Randall Degges on the {okta} blog. It gave a lot of informations lot more easy to understand than by reading the RFC directly.
Public v2 tokens
PASETO tokens are structured as follow
version.purpose.payload.optional_footer
Even if the RFC is still in draft, there is already two versions of the protocol and the v2 is the recommanded one. That's why I'm gonna talk about the v2 in this article.
So, in the first position of the token, you need to precise the version used. The purpose can be "local" of "public".
When you are using a local purpose, the encryption and decryption of the token is done by using a ChaCha20-Poly1305 algorithm. It's a symetric shared key.
In the public purpose case, the token is not encrypted but signed. The process of signing and verifying is done by the usage of ED25519 algorithm with a pair of public and private key. Public token do not prevent the contents from being read but, by signing mecanism prevent it from being modified.
The payload is basically the content of the token. It's a JSON object containing whatever you want. Each key of you JSON is called a "claim". There is some claims that you cannot use. They are reserved for the usage of PASETO.
+-----+------------+--------+-------------------------------------+
| Key | Name | Type | Example |
+-----+------------+--------+-------------------------------------+
| iss | Issuer | string | {"iss":"paragonie.com"} |
| sub | Subject | string | {"sub":"test"} |
| aud | Audience | string | {"aud":"pie-hosted.com"} |
| exp | Expiration | DtTime | {"exp":"2039-01-01T00:00:00+00:00"} |
| nbf | Not Before | DtTime | {"nbf":"2038-04-01T00:00:00+00:00"} |
| iat | Issued At | DtTime | {"iat":"2038-03-17T00:00:00+00:00"} |
| jti | Token ID | string | {"jti":"87IFSGFgPNtQNNuw0AtuLttP"} |
| kid | Key-ID | string | {"kid":"stored-in-the-footer"} |
+-----+------------+--------+-------------------------------------+
Finally, the footer can contain any unencrypted optional data. The payload and footer must be encoded using base64 without the "=" padding.
Don't hesitate to look on the RFC for more detailed informations about each part of the token. I intentionally stay light in the description of how PASETO is working, assuming that you already have this knowledges.
Generate keypair
The Ed25519 algorithm is, today, the most recommended public-key algorithm. It's using elliptic curve cryptography that offers a better security with faster performance. You are probably using RSA for generating SSH keys today. The security with RSA depend on the key size. With a 1024 bit length, a key is considered weak. You should use at least 3072 or 4096-bit length.
Compared to RSA with 544 characteds for a 3072 key, Ed25519 public key is containing only 68 characters.
You have several way to generate a Ed25519 keypair. OpenSSL is implementing this algorithm but libsodium is widly recommended and used by many libraries. For the example, we are using this link.
You can generate a keypair with your own seed or by a random one. When the keys are generated, you can see that they are base64 encoded.
Example private key:
T2fvv1usCsAcMPNZ5gPHnUWyuB1QPByLD62kaaO51Q4tnzMafruKhw3GQqI1G61H/4K4jwlq/z6F71Q/7Zz3uA==
Example public key:
LZ8zGn67iocNxkKiNRutR/+CuI8Jav8+he9UP+2c97g=
If we convert this key from base64 to hexadecimal, you can see that the private key is containing also the public key.
Private key hex encoded:
4f67efbf5bac0ac01c30f359e603c79d45b2b81d503c1c8b0fada469a3b9d50e 2d9f331a7ebb8a870dc642a2351bad47ff82b88f096aff3e85ef543fed9cf7b8
Public key hex encoded:
2d9f331a7ebb8a870dc642a2351bad47ff82b88f096aff3e85ef543fed9cf7b8
Create a token from the generated private key
On the PASETO website, you will find a list of different libraries for many different languages. For the example, I use the JAVA library JPaseto.
First of all, you can add the following dependencies in your pom file:
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-api</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-impl</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-gson</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-bouncy-castle</artifactId>
<version>0.5.0</version>
</dependency>
You should store your key on a secure place, depending of your environnment. Here, I'm importing it directly on the code for the example, with the key encoded in hexadecimal.
First thing to do is to add the BouncyCastleProvider to the java Security provider list. Then you need to decode your key to byte array and the rest of the code speek for itself ;-)
private PrivateKey getPrivateKey() {
Security.addProvider(new BouncyCastleProvider());
byte[] pvKey = FormatUtils.decodeHex(privateKey);
try {
KeyFactory keyFact = KeyFactory.getInstance("Ed25519");
PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), new DEROctetString(pvKey));
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
return keyFact.generatePrivate(pkcs8KeySpec);
} catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
throw new Error(e);
}
}
Now, we have a stadard Java Security Private Key. We can create a new V2 public token as follow
public String signToken() {
PrivateKey privateKey = getPrivateKey();
return Pasetos.V2.PUBLIC.builder()
.setSerializer(getSerializer())
.setIssuer("example-issuer")
.setSubject("example-subject")
.setIssuedAt(Instant.now())
.claim("yourCustomClaim", "value")
.claim("otherCustomClaim", "otherValue")
.setPrivateKey(privateKey)
.setFooter("optionalFooterAsString")
.compact();
}
And that's it ! Your function is now returning your public v2 PASETO token :
v2.public.eyJzdWIiOiJleGVtcGxlLXN1YmplY3QiLCJvdGhlckN1c3RvbUNsYWltIjoib3RoZXJWYWx1ZSIsImlzcyI6ImV4ZW1wbGUtaXNzdWVyIiwiaWF0IjoiMjAyMC0xMi0xMVQxODozNTo1MiswMTAwIiwieW91ckN1c3RvbUNsYWltIjoidmFsdWUifQXSAfHKwBOgqUJJBjZAElO3c4MSiWrx_EHnRcZmxKtZTjF8xu9OjRWyBelUZSCBLJhnIIce4fZ1kUep-IMT9Q4.b3B0aW9uYWxGb290ZXJBc1N0cmluZw
Read and verify a token with the public key
Like for the private key, JPaseto is using java security PublicKey object. So, we need to recreate one from our hex encoded public key
private PublicKey getPublicKey() {
Security.addProvider(new BouncyCastleProvider());
byte[] pvKey = FormatUtils.decodeHex(publicKey);
try {
KeyFactory keyFact = KeyFactory.getInstance("Ed25519");
SubjectPublicKeyInfo publicKeyInfo = new SubjectPublicKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), pvKey);
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyInfo.getEncoded());
return keyFact.generatePublic(x509EncodedKeySpec);
} catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
throw new Error(e);
}
}
We can now use this for read the token and verify that the public key correspond to the private key who sign the token.
public boolean verifyToken(String token) {
try {
PublicKey publicKey = getPublicKey();
Pasetos.parserBuilder()
.setPublicKey(publicKey)
.build()
.parse(token);
return true;
} catch (PasetoException e) {
return false;
}
}
JPaseto work by throwing exceptions when a problem is encountered. The catched class in the above example, PasetoException, is the main exception. With this one, you can (like Pokemon) catch them all. Many others child exceptions can be thrown for specific case.
Take a look on the JPaseto documentation form more information.
You can find all the examples and more detailed implemantation on my GitHub: https://github.com/mettany/pasesto-example