All PASETO payloads must be a JSON-encoded object represented as a UTF-8 encoded string. The topmost JSON object should be an object, map, or associative array (select appropriate for your language), not a flat array or list.
PASETO library implementors MUST ensure uniqueness of object key names.
Valid:
{"foo":"bar"}
{"foo":"bar","baz":12345,"678":["a","b","c"]}
{}
Invalid:
[{"foo":"bar"}]
["foo"]
{0: "test"}
[]
- (Empty string)
{"foo":"bar","foo":"baz"}
If non-UTF-8 character sets are desired for some fields, implementors are encouraged to use Base64url encoding to preserve the original intended binary data, but still use UTF-8 for the actual payloads.
PASETO library implementations MUST implement some means of preventing type confusion bugs between different cryptography keys. For example:
- Prepending each key in memory with a magic byte to serve as a type indicator (distinct for every combination of version and purpose).
- In object-oriented programming languages, using separate classes for each cryptography key object that may share an interface or common base class.
It MUST NOT be possible for a user to take a known public key (used by public tokens), and generate a local token with the same key that any PASETO implementations will accept.
See Algorithm Lucidity for more information and guidance.
PASETO places no restrictions on the contents of the authenticated footer. The footer's contents MAY be JSON-encoded (as is the payload), but it doesn't have to be.
The footer contents is intended to be free-form and application-specific.
Implementations that allow users to store JSON-encoded objects in the footer MUST give users some mechanism to validate the footer before decoding.
Some example parser rules include:
- Enforcing a maximum length of the JSON-encoded string.
- Enforcing a maximum depth of the decoded JSON object. (Recommended default: Only 1-dimensional objects.)
- Enforcing the maximum number of named keys within an object.
The motivation for these additional rules is to mitigate the following security risks:
- Stack overflows in JSON parsers caused by too much recursion.
- Denial-of-Service attacks enabled by hash-table collisions.
Arbitrary-depth JSON strings can be a risk for stack overflows in some JSON parsing libraries. One mitigation to this is to enforce an upper limit on the maximum stack depth. Some JSON libraries do not allow you to configure this upper limit, so you're forced to take matters into your own hands.
A simple way of enforcing the maximum depth of a JSON string without having to parse it with your JSON library is to employ the following algorithm:
- Create a copy of the JSON string with all
\"
sequences and whitespace characters removed. This will prevent weird edge cases in step 2. - Use a regular expression to remove all quoted strings and their contents.
For example, replacing
/"[^"]+?"([:,\}\]])/
with the first match will strip the contents of any quoted strings. - Remove all characters except
[
,{
,}
, and]
. - If you're left with an empty string, return
1
. - Initialize a variable called
depth
to1
. - While the stripped variable is not empty and not equal to the output
of the previous iteration, remove all
{}
and[]
pairs, then incrementdepth
. - If you end up with a non-empty string, you know you have invalid JSON:
Either you have a
[
that isn't paired with a]
, or a{
that isn't paired with a}
. Throw an exception. - Return
depth
.
An example of this logic implemented in TypeScript is below:
function getJsonDepth(data: string): number {
// Step 1
let stripped = data.replace(/\\"/g, '').replace(/\s+/g, '');
// Step 2
stripped = stripped.replace(/"[^"]+"([:,\}\]])/g, '$1');
// Step 3
stripped = stripped.replace(/[^\[\{\}\]]/g, '');
// Step 4
if (stripped.length === 0) {
return 1;
}
// Step 5
let previous = '';
let depth = 1;
// Step 6
while (stripped.length > 0 && stripped !== previous) {
previous = stripped;
stripped = stripped.replace(/({}|\[\])/g, '');
depth++;
}
// Step 7
if (stripped.length > 0) {
throw new Error(`Invalid JSON string`);
}
// Step 8
return depth;
}
Hash-collision Denial of Service attacks (Hash-DoS) is made possible by creating a very large number of keys that will hash to the same value, with a given hash function (e.g., djb33).
One mitigation strategy is to limit the number of keys contained within an object (at any arbitrary depth).
The easiest way is to count the number of times you encounter a ":
token that isn't followed by a backslash (to side-step corner-cases where
JSON is encoded as a string inside a JSON value).
Here's an example implementation in TypeScript:
/**
* Split the string based on the number of `":` pairs without a preceding
* backslash, then return the number of pieces it was broken into.
*/
function countKeys(json: string): number {
return json.split(/[^\\]":/).length;
}
This has been moved to a separate document to make it easier to locate: Registered Claims.
Some systems need to support key rotation, but since the payloads of a local
token are always encrypted, you can't just drop a kid
claim inside the payload.
Instead, users should store Key-ID claims (kid
) in the unencrypted footer.
For example, if you set the footer to {"kid":"gandalf0"}
, you can read it without
needing to first decrypt the token (which would in turn knowing which key to use to
decrypt the token).
PASERK, a PASETO extension, defines a
universal and unambiguous way to calculate key identifiers for a PASETO key. See
the specification for PASERK's ID
operation
for more information. PASERK is the RECOMMENDED way to serialize Key IDs.
Implementations should feel free to provide a means to extract the footer from a token, before decryption, since the footer is used in the calculation of the authentication tag for the encrypted payload.
Users should beware that, until this authentication tag has been verified, the footer's contents are not authenticated.
While a key identifier can generally be safely used for selecting the cryptographic
key used to decrypt and/or verify payloads before verification, provided that they
key-id
is a public number that is associated with a particular key which is not
supplied by attackers, any other fields stored in the footer MUST be distrusted
until the payload has been verified.
IMPORTANT: Key identifiers MUST be independent of the actual keys used by Paseto.
For example, you MUST NOT just drop the public key into the footer for
a public
token and have the recipient use the provided public key.
Doing so would allow an attacker to simply replace the public key with
one of their own choosing, which will cause the recipient to simply
accept any signature for any message as valid, which defeats the
security goals of public-key cryptography.
Instead, it's recommended that implementors and users use a unique identifier for each key (independent of the cryptographic key's contents itself) that is used in a database or other key-value store to select the apppropriate cryptographic key. These search operations MUST fail closed if no valid key is found for the given key identifier.
The payload processing SHOULD NOT change after version 1.0.0 of the reference implementation has been tagged, signed, and released; only the cryptography protocols will receive new versions.
In the event that this turns out to not be true, we will change the first letter
of the version identifier (v
) to another ASCII-compatible alphanumeric character.
However, we hope to never need to do this.