Skip to content

Commit

Permalink
Merge pull request #127 from paragonie/new-versions
Browse files Browse the repository at this point in the history
Initial Draft for PASETO v3/v4 specifications
  • Loading branch information
paragonie-security authored Jul 30, 2021
2 parents 149a8eb + c349cab commit aa05b70
Show file tree
Hide file tree
Showing 7 changed files with 890 additions and 21 deletions.
104 changes: 91 additions & 13 deletions docs/01-Protocol-Versions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,61 @@
This document describes the cryptography and encoding rules for Paseto protocol versions,
to assist in cross-platform library development.

# Rules for Current and Future Protocol Versions
## Naming Conventions

The cryptography protocol version is named using this convention: `/^Version \d$/`.
When we discuss "Version 4" spelled out, we're talking about the cryptography
without any regard to the underlying encoding format for the payload containing
the claims.

The token format is named using this convention: `/^v\d([a-z]+?)$/`. When
no optional suffix is provided, this describes a PASETO token using JSON
to encode claims, along with the corresponding Version (see previous paragraph)
to protect those claims.

The intent is that the cryptographic format ("Version 3", "Version 4") can be
reused for arbitrary payloads, but the token format ("v3", "v4") refers to a
specific encoding of claims under-the-hood.

If this is confusing, just know that most of the time, you only need to deal
with the complete token (n.b., some permutation of {`v1`, `v2`, `v3`, `v4`}
and {`local`, `public`}).
The cryptographic layer (`Version 1`, `Version 2`, `Version 3`, `Version 4`)
is mostly for cryptographers to argue about.

## Rules for Current and Future Protocol Versions

1. Everything must be authenticated. Attackers should never be allowed the opportunity
to alter messages freely.
* If encryption is specified, unauthenticated modes (e.g. AES-CBC) are forbidden.
* The nonce or initialization vector must be covered by the authentication
tag, not just the ciphertext.
* Some degree of nonce-misuse resistance should be provided by any future schemes.
2. Non-deterministic, stateful, and otherwise dangerous signature schemes (e.g. ECDSA
without RFC 6979, XMSS) are forbidden.
2. Non-deterministic, stateful, and otherwise dangerous signature schemes (e.g. ~~ECDSA
without RFC 6979,~~ XMSS) are forbidden.
* ECDSA without RFC 6979 is permitted, but *only* when a CSPRNG is reliably available.
If this cannot be guaranteed, you **MUST NOT** implement ECDSA without RFC 6979.
3. Public-key cryptography must be IND-CCA2 secure to be considered for inclusion.
* This means no RSA with PKCS1v1.5 padding, textbook RSA, etc.
4. By default, libraries should only allow the two most recent versions to be used.
* If there are only two versions, that means `v1` and `v2`.
* If a future post-quantum `v3` is defined, `v1` should no longer be accepted.
* If an additional version `v4` is defined, `v2` should also no longer be accepted.
4. By default, libraries should only allow the two most recent versions in a family
to be used.
* The NIST family of versions is `Version 1` and `Version 3`.
* The Sodium family of versions is `Version 2` and `Version 4`.
* If a future post-quantum `Version 5` (NIST) and/or `Version 6` (Sodium) is defined,
`Version 1` and `Version 2` should no longer be accepted.
* This is a deviation from the **original** intent of this rule, to encapsulate
the fact that we have parallel versions. In the future, we expect this to converge
to one family of versions.
5. New versions will be decided and formalized by the PASETO developers.
* User-defined homemade protocols are discouraged. If implementors wish to break
this rule and define their own custom protocol suite, they must NOT continue
the {`v1`, `v2`, ... } series naming convention.
* Any version identifiers that match the regular expression, `/^v[0-9\.]+$/` are
reserved by the PASETO development team.
the {`v1`, `v2`, ... } series naming convention for tokens.
* Any version identifiers that match the regular expression,
`/^v[0-9\-\.]+([a-z]+?)/` are reserved by the PASETO development team.

# Versions

## Version 1: Compatibility Mode
## Version 1: NIST Compatibility

See [the version 1 specification](Version1.md) for details. At a glance:

Expand All @@ -41,9 +70,10 @@ See [the version 1 specification](Version1.md) for details. At a glance:
* The nonce calculated from HMAC-SHA384(message, `random_bytes(32)`)
truncated to 32 bytes, during encryption only
* The HMAC covers the header, nonce, and ciphertext
* It also covers the footer, if provided
* Reference implementation in [Version1.php](https://github.com/paragonie/paseto/blob/master/src/Protocol/Version1.php):
* See `aeadEncrypt()` for encryption
* See `aeadDncrypt()` for decryption
* See `aeadDecrypt()` for decryption
* **`v1.public`**: Asymmetric Authentication (Public-Key Signatures):
* 2048-bit RSA keys
* RSASSA-PSS with
Expand All @@ -61,7 +91,7 @@ Version 1 is recommended only for legacy systems that cannot use modern cryptogr

See also: [Common implementation details for all versions](Common.md).

## Version 2: Recommended
## Version 2: Sodium Original

See [the version 2 specification](Version2.md) for details. At a glance:

Expand All @@ -84,3 +114,51 @@ See [the version 2 specification](Version2.md) for details. At a glance:
* See `verify()` for signature verification

See also: [Common implementation details for all versions](Common.md).

## Version 3: NIST Modern

See [the version 3 specification](Version3.md) for details. At a glance:

* **`v3.local`**: Symmetric Authenticated Encryption:
* AES-256-CTR + HMAC-SHA384 (Encrypt-then-MAC)
* Key-splitting: HKDF-SHA384
* Info for encryption key: `paseto-encryption-key`
The encryption key and implicit counter nonce are both returned
from HKDF in this version.
* Info for authentication key: `paseto-auth-key-for-aead`
* 32-byte nonce (no longer prehashed), passed entirely to HKDF
(as part of the `info` tag, rather than as a salt).
* The HMAC covers the header, nonce, and ciphertext
* It also covers the footer, if provided
* It also covers the implicit assertions, if provided
* **`v3.public`**: Asymmetric Authentication (Public-Key Signatures):
* ECDSA over NIST P-384, with SHA-384,
using [RFC 6979 deterministic k-values](https://tools.ietf.org/html/rfc6979)
(if reasonably practical; otherwise a CSPRNG **MUST** be used).
Hedged signatures are allowed too.
* The public key is also included in the PAE step, to ensure
`v3.public` tokens provide Exclusive Ownership.

See also: [Common implementation details for all versions](Common.md).

## Version 4: Sodium Modern

See [the version 4 specification](Version4.md) for details. At a glance:

* **`v4.local`**: Symmetric Authenticated Encryption:
* XChaCha20 + BLAKE2b-MAC (Encrypt-then-MAC)
* Key-splitting: BLAKE2b
* Info for encryption key: `paseto-encryption-key`
The encryption key and implicit counter nonce are both returned
from BLAKE2b in this version.
* Info for authentication key: `paseto-auth-key-for-aead`
* 32-byte nonce (no longer prehashed), passed entirely to BLAKE2b.
* The BLAKE2b-MAC covers the header, nonce, and ciphertext
* It also covers the footer, if provided
* It also covers the implicit assertions, if provided
* **`v4.public`**: Asymmetric Authentication (Public-Key Signatures):
* Ed25519 (EdDSA over Curve25519)
* Signing: `sodium_crypto_sign_detached()`
* Verifying: `sodium_crypto_sign_verify_detached()`

See also: [Common implementation details for all versions](Common.md).
213 changes: 213 additions & 0 deletions docs/01-Protocol-Versions/Version3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# Paseto Version 3

## GetNonce

Throw an exception. We don't do this in version 3.

## Encrypt

Given a message `m`, key `k`, and optional footer `f` (which defaults to empty
string), and an optional implicit assertion `i` (which defaults to empty string):

1. Set header `h` to `v3.local.`
2. Generate 32 random bytes from the OS's CSPRNG to get the nonce, `n`.
3. Split the key into an Encryption key (`Ek`) and Authentication key (`Ak`),
using HKDF-HMAC-SHA384, with `n` appended to the info rather than the salt.
* The output length **MUST** be 48 for both key derivations.
* The derived key will be the leftmost 32 bytes of the first HKDF derivation.

The remaining 16 bytes of the first key derivation (from which `Ek` is derived)
will be used as a counter nonce (`n2`):
```
tmp = hkdf_sha384(
len = 48,
ikm = k,
info = "paseto-encryption-key" || n,
salt = NULL
);
Ek = tmp[0:32]
n2 = tmp[32:]
Ak = hkdf_sha384(
len = 48,
ikm = k,
info = "paseto-auth-key-for-aead" || n,
salt = NULL
);
```
5. Encrypt the message using `AES-256-CTR`, using `Ek` as the key and `n2` as the nonce.
We'll call the encrypted output of this step `c`:
```
c = aes256ctr_encrypt(
plaintext = m,
nonce = n2
key = Ek
);
```
6. Pack `h`, `n`, `c`, `f`, and `i` together using
[PAE](https://github.com/paragonie/paseto/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding)
(pre-authentication encoding). We'll call this `preAuth`.
7. Calculate HMAC-SHA384 of the output of `preAuth`, using `Ak` as the
authentication key. We'll call this `t`.
8. If `f` is:
* Empty: return "`h` || base64url(`n` || `c` || `t`)"
* Non-empty: return "`h` || base64url(`n` || `c` || `t`) || `.` || base64url(`f`)"
* ...where || means "concatenate"
* Note: `base64url()` means Base64url from RFC 4648 without `=` padding.

## Decrypt

Given a message `m`, key `k`, and optional footer `f`
(which defaults to empty string), and an optional
implicit assertion `i` (which defaults to empty string):

1. If `f` is not empty, implementations **MAY** verify that the value appended
to the token matches some expected string `f`, provided they do so using a
constant-time string compare function.
* If `f` is allowed to be a JSON-encoded blob, implementations **SHOULD** allow
users to provide guardrails against invalid JSON tokens.
See [this document](../03-Implementation-Guide/01-Payload-Processing.md#optional-footer)
for specific guidance and example code.
2. Verify that the message begins with `v3.local.`, otherwise throw an
exception. This constant will be referred to as `h`.
* **Future-proofing**: If a future PASETO variant allows for encodings other
than JSON (e.g., CBOR), future implementations **MAY** also permit those
values at this step (e.g. `v3c.local.`).
3. Decode the payload (`m` sans `h`, `f`, and the optional trailing period
between `m` and `f`) from base64url to raw binary. Set:
* `n` to the leftmost 32 bytes
* `t` to the rightmost 48 bytes
* `c` to the middle remainder of the payload, excluding `n` and `t`
4. Split the key (`k`) into an Encryption key (`Ek`) and an Authentication key
(`Ak`), `n` appended to the HKDF info.
* For encryption keys, the **info** parameter for HKDF **MUST** be set to
**paseto-encryption-key**.
* For authentication keys, the **info** parameter for HKDF **MUST** be set to
**paseto-auth-key-for-aead**.
* The output length **MUST** be 48 for both key derivations.
The leftmost 32 bytes of the first key derivation will produce `Ek`, while
the remaining 16 bytes will be the AES nonce `n2`.

```
tmp = hkdf_sha384(
len = 48,
ikm = k,
info = "paseto-encryption-key" || n,
salt = NULL
);
Ek = tmp[0:32]
n2 = tmp[32:]
Ak = hkdf_sha384(
len = 48,
ikm = k,
info = "paseto-auth-key-for-aead" || n,
salt = NULL
);
```
5. Pack `h`, `n`, `c`, `f`, and `i` together (in that order) using
[PAE](https://github.com/paragonie/paseto/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding).
We'll call this `preAuth`.
6. Recalculate HMAC-SHA-384 of `preAuth` using `Ak` as the key. We'll call this `t2`.
7. Compare `t` with `t2` using a constant-time string compare function. If they
are not identical, throw an exception.
* You **MUST** use a constant-time string compare function to be compliant.
If you do not have one available to you in your programming language/framework,
you MUST use [Double HMAC](https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy).
* Common utilities that were not intended for cryptographic comparisons, such as
Java's `Array.equals()` or PHP's `==` operator, are explicitly forbidden.
8. Decrypt `c` using `AES-256-CTR`, using `Ek` as the key and `n2` as the nonce,
then return the plaintext.
```
return aes256ctr_decrypt(
cipherext = c,
nonce = n2
key = Ek
);
```

## Sign

Given a message `m`, 384-bit ECDSA secret key `sk`, an optional footer `f`
(which defaults to empty string), and an optional implicit assertion `i`
(which defaults to empty string):

1. Set `h` to `v3.public.`
2. Pack `pk`, `h`, `m`, `f`, and `i` together using
[PAE](https://github.com/paragonie/paseto/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding)
(pre-authentication encoding). We'll call this `m2`.
* Note: `pk` is the public key corresponding to `sk` (which **MUST** use
[point compression](https://www.secg.org/sec1-v2.pdf)). `pk` **MUST** be 49
bytes long, and the first byte **MUST** be `0x02` or `0x03` (depending on
[the least significant bit of Y](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.202.2977&rep=rep1&type=pdf);
section 4.3.6, step 2.2).
The remaining bytes **MUST** be the X coordinate, using big-endian byte order.
3. Sign `m2` using ECDSA over P-384 and SHA-384 with the private key `sk`.
We'll call this `sig`. The output of `sig` MUST be in the format `r || s`
(where `||`means concatenate), for a total length of 96 bytes.
* Signatures **SHOULD** use deterministic nonces ([RFC 6979](https://tools.ietf.org/html/rfc6979))
if possible, to mitigate the risk of [k-value reuse](https://blog.trailofbits.com/2020/06/11/ecdsa-handle-with-care/).
* If RFC 6979 is not available in your programming language, ECDSA **MUST** use a CSPRNG
to generate the k-value.
* Hedged signatures (RFC 6979 + additional randomness to provide resilience to fault attacks)
are allowed.
```
sig = crypto_sign_ecdsa_p384(
message = m2,
private_key = sk
);
```
4. If `f` is:
* Empty: return "`h` || base64url(`m` || `sig`)"
* Non-empty: return "`h` || base64url(`m` || `sig`) || `.` || base64url(`f`)"
* ...where || means "concatenate"
* Note: `base64url()` means Base64url from RFC 4648 without `=` padding.

### ECDSA Public Key Point Compression

Given a public key consisting of two coordinates (X, Y):

1. Set the header to `0x02`.
2. Take the least significant bit of `Y` and add it to the header.
3. Append the X coordinate (in big-endian byte order) to the header.

In pseudocode:

```
lsb(y):
return y[y.length - 1] & 1
pubKeyCompress(x, y):
header = [0x02 + lsb(y)]
return header.concat(x)
```

## Verify

Given a signed message `sm`, ECDSA public key `pk` (which **MUST** use
[point compression](https://www.secg.org/sec1-v2.pdf) (Section 2.3.3)),
and optional footer `f` (which defaults to empty string), and an optional
implicit assertion `i` (which defaults to empty string):

1. If `f` is not empty, implementations **MAY** verify that the value appended
to the token matches some expected string `f`, provided they do so using a
constant-time string compare function.
2. Verify that the message begins with `v3.public.`, otherwise throw an
exception. This constant will be referred to as `h`.
3. Decode the payload (`sm` sans `h`, `f`, and the optional trailing period
between `m` and `f`) from base64url to raw binary. Set:
* `s` to the rightmost 96 bytes
* `m` to the leftmost remainder of the payload, excluding `s`
4. Pack `pk`, `h`, `m`, `f`, and `i` together (in that order) using PAE (see
[PAE](https://github.com/paragonie/paseto/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding).
We'll call this `m2`.
* `pk` **MUST** be 49 bytes long, and the first byte **MUST** be `0x02` or `0x03`
(depending on the sign of the Y coordinate). The remaining bytes **MUST** be
the X coordinate, using big-endian byte order.
5. Use ECDSA to verify that the signature is valid for the message:
```
valid = crypto_sign_ecdsa_p384_verify(
signature = s,
message = m2,
public_key = pk
);
```
6. If the signature is valid, return `m`. Otherwise, throw an exception.
Loading

0 comments on commit aa05b70

Please sign in to comment.