Repository: pyca/cryptography

Issue:
OCB mode (AESOCB3) should support nonce sizes up to 15 bytes
[`cryptography.hazmat.primitives.ciphers.aead.AESOCB3._check_params`](https://github.com/pyca/cryptography/blob/d98e0bd355544a28cc36aba3a05d6e44604ac289/src/cryptography/hazmat/primitives/ciphers/aead.py#L281) requires that the nonce size be fixed at 12 bytes, or 96 bits.

However, [RFC 7253 §4.2](https://datatracker.ietf.org/doc/html/rfc7253#section-4.2) suggests that the nonce can go up to 120 bits.

The OpenSSL backend to cryptography doesn't appear to be the source of the limitation: [OpenSSL's OCB interface](https://github.com/openssl/openssl/blob/master/doc/man3/EVP_EncryptInit.pod#gcm-and-ocb-modes) suggests that `EVP_CTRL_AEAD_SET_IVLEN` permits setting a nonce up to 15 bytes (120 bits), in line with RFC 7253.  (This has to be done between the first `EVP_CipherInit_ex` (where the `iv` parameter should be `NULL`) and the subsequent `EVP_CipherInit_ex` invocation that sets the `iv`, if i'm understanding the OpenSSL interface correctly.)

The [next revision of the OpenPGP standard](https://datatracker.ietf.org/doc/draft-ietf-openpgp-crypto-refresh/) looks likely to encourage the use of OCB, and is in discussion about [how many bits to use for the nonce](https://gitlab.com/openpgp-wg/rfc4880bis/-/issues/83).  If it goes with anything other than 96, any python implementation of OpenPGP (e.g. [pgpy](https://github.com/SecurityInnovation/PGPy/)) would need to write their own OCB wrapper around even lower-level `hazmat` primitives, or use some python module other than `cryptography`.

It would be great if cryptography would be able to accept nonces up to 120 bits (15 bytes) to accomodate this use case.


Discussion:
The restriction here is largely a function of test vectors in the RFC only containing 96-bit nonces. Allowing a range (but still enforcing a minimum nonce size) up to 120-bits is certainly possible, although I see OpenSSL itself does not appear to have tests for nonce lengths of 120-bit 🤔.
Looks like on https://gitlab.com/openpgp-wg/rfc4880bis/-/issues/83 they've decided to stick with 120 bits. I'd like to find a source of test vectors here before we patch our implementation to allow 12-15 bytes. I see no reason to allow shorter nonces without a concrete reason since the AEAD interfaces aren't intended as fully generalized ciphers anyway.
I agree with you that allowing only larger nonce sizes is a reasonable, conservative approach.  For OpenPGP, there's certainly no need for smaller.  I'm happy to ask Krovetz and Rogaway (the authors of the RFC and OCB3 itself) whether they can point to test vectors with larger nonce sizes.
That'd be great. Rogaway's current documentation just points to the RFC, and while alternate nonce lengths may not be a huge risk I never underestimate the ability of software to be incorrect 😂 
I've written to Rogaway and Krovetz, but haven't heard back yet.

In the meantime, i've reproduced the first block of the RFC's test vectors from source using [the same codepaths that I believe Krovetz used](https://gitlab.com/dkg/ocb-test-vectors).  I had to fix a bit of ruby-related bitrot first, and fiddle with some formatting, but it looks clean now.

After parameterizing the code, it also produces [test vectors with a 15-byte nonce as well](https://gitlab.com/dkg/ocb-test-vectors/-/blob/main/test-vector-1-nonce120.txt).  This is all done with a codepath that doesn't use OpenSSL at all, afaict, so if you want to try to use those test vectors against your calculations for the same values, we can at least confirm independent re-implementation.

I welcome any critique on what i've done there.  Don't be afraid to tell me i've screwed it up somehow :)

And if anyone has any interest in helping sort out a similar process for the two remaining sections of the RFC's test vectors, i'd welcome contributions too.
I haven’t taken a look yet, but we have precedent for generating test vectors using alternate implementations so this is a totally reasonable approach.
Ted Krovetz got back to me and supplied some C code with OpenSSL that confirmed the first section of the test vectors, and was capable of regenerating at least part of the third section of test vectors (the "omnibus") .

I tweaked the parameters and convinced it to generate a 120-bit nonce version of section 3.

Then I fiddled with Krovetz's C a bit more and convinced it to regenerate section 2, and parameterized it as well into generating an equivalent version with a 120-bit nonce.

Please [take a look](https://gitlab.com/dkg/ocb-test-vectors) and let me know if this is what you need to make python-cryptography flexible enough to be able to use 120-bit nonces with AES OCB.

Resolving patch:
diff --git a/src/cryptography/hazmat/primitives/ciphers/aead.py b/src/cryptography/hazmat/primitives/ciphers/aead.py
--- a/src/cryptography/hazmat/primitives/ciphers/aead.py
+++ b/src/cryptography/hazmat/primitives/ciphers/aead.py
@@ -287,5 +287,5 @@ def _check_params(
         utils._check_byteslike("nonce", nonce)
         utils._check_bytes("data", data)
         utils._check_bytes("associated_data", associated_data)
-        if len(nonce) != 12:
-            raise ValueError("Nonce must be 96-bits (12 bytes)")
+        if len(nonce) < 12 or len(nonce) > 15:
+            raise ValueError("Nonce must be between 12 and 15 bytes")


Test patch:
diff --git a/tests/hazmat/primitives/test_aead.py b/tests/hazmat/primitives/test_aead.py
--- a/tests/hazmat/primitives/test_aead.py
+++ b/tests/hazmat/primitives/test_aead.py
@@ -495,10 +495,20 @@ def test_data_too_large(self):
             aesocb3.encrypt(nonce, b"", FakeData())
 
     def test_vectors(self, backend, subtests):
-        vectors = load_vectors_from_file(
-            os.path.join("ciphers", "AES", "OCB3", "rfc7253.txt"),
-            load_nist_vectors,
-        )
+        vectors = []
+        for f in [
+            "rfc7253.txt",
+            "test-vector-1-nonce104.txt",
+            "test-vector-1-nonce112.txt",
+            "test-vector-1-nonce120.txt",
+        ]:
+            vectors.extend(
+                load_vectors_from_file(
+                    os.path.join("ciphers", "AES", "OCB3", f),
+                    load_nist_vectors,
+                )
+            )
+
         for vector in vectors:
             with subtests.test():
                 nonce = binascii.unhexlify(vector["nonce"])
@@ -557,7 +567,7 @@ def test_invalid_nonce_length(self, backend):
         with pytest.raises(ValueError):
             aesocb3.encrypt(b"\x00" * 11, b"hi", None)
         with pytest.raises(ValueError):
-            aesocb3.encrypt(b"\x00" * 13, b"hi", None)
+            aesocb3.encrypt(b"\x00" * 16, b"hi", None)
 
     def test_bad_key(self, backend):
         with pytest.raises(TypeError):