paseto-standard / paseto-spec

Specification for Platform Agnostic SEcurity TOkens (PASETO)
165 stars 9 forks source link

Clarify payload destructuring #21

Open aidantwoods opened 2 years ago

aidantwoods commented 2 years ago

For consistency, should the spec specify how to handle cases where a PASETO payload is received with an insufficient length?

Language is largely similar across the board, but using v4 decrypt as a reference, see step 4: https://github.com/paseto-standard/paseto-spec/blob/6143e466f8e69204d10ecc7e4e68e49a05b743b4/docs/01-Protocol-Versions/Version4.md#L73-L84

The reference implementation notably does not make length checks here, which can result in negative lengths in the calculation of $ciphertext, or negative offsets in the calculation of $mac:

https://github.com/paragonie/paseto/blob/6e3ce7fb5dfb2da512f38877e0b6e987ab519d8b/src/Protocol/Version4.php#L467-L474

        $len = Binary::safeStrlen($decoded);
        $nonce = Binary::safeSubstr($decoded, 0, self::NONCE_SIZE);
        $ciphertext = Binary::safeSubstr(
            $decoded,
            self::NONCE_SIZE,
            $len - (self::NONCE_SIZE + self::MAC_SIZE)
        );
        $mac = Binary::safeSubstr($decoded, $len - self::MAC_SIZE);

Based on the implementation of Binary::safeSubstr when encountering negative offsets, this is ultimately identical to PHP's (i.e. counting backwards from the end of the string). This can lead to curious situations where (dependent on length) the bytes from the mac can be overlapped with the nonce, and a subset of the mac used as the ciphertext.

e.g.

$decoded = str_repeat(1, 31) . str_repeat(3, 32);           // 111111111111111111111111111111133333333333333333333333333333333
$len = Binary::safeStrlen($decoded); // 63
$nonce = Binary::safeSubstr($decoded, 0, self::NONCE_SIZE); // 11111111111111111111111111111113
$ciphertext = Binary::safeSubstr(                           // 333333333333333333333333333333
    $decoded,
    self::NONCE_SIZE,
    $len - (self::NONCE_SIZE + self::MAC_SIZE)
);
$mac = Binary::safeSubstr($decoded, $len - self::MAC_SIZE); // 33333333333333333333333333333333

Aside from overlap (which in theory means that certain payloads are parsed identically even though they are different lengths), lengths of the interpreted ciphertext and mac are also fairly unintuitive here around certain boundaries (with lengths decreasing, and then increasing again). I don't think that this is abusable in practice, but it is probably an odd enough behaviour that it should be avoided?

Would a sensible behaviour in this example be to abort the decryption if $len <= self::NONCE_SIZE + self::MAC_SIZE? Obviously having a zero length ciphertext isn't likely to be legitimate, but we at least dispense of negative offsets here. I don't believe this early abort will have any security impact because the decision is made by examining the ciphertext and associated data alone, and is not interacting with the plaintext (e.g. so wouldn't be abusable in the same way as padding errors in a padding oracle attack). Still, I think it probably makes sense to formalise the early abort here as part of the spec?

For public modes, when calculating the signature offset based on the length subtraction from the end of the string, I think for similar reasons it would make sense to ensure that the signature offset is non-negative (i.e. that the payload is at least "signatureLength" long?)