Repository: secdev/scapy

Issue:
scapy.layers.tls.crypto.hkdf.TLS13_HKDF crashes when cryptography module is missing
### Brief description

scapy.layers.tls.crypto.hkdf.TLS13_HKDF crashes in multiple environments

### Scapy version

2.5.0

### Python version

3.11.2

### Operating system

macOS Ventura 13.3.1 (with M1 chip)

### Additional environment information

_No response_

### How to reproduce

```
$ python --version
Python 3.11.2
$ pip install scapy
$ pip show scapy
Name: scapy
Version: 2.5.0
...
$ python
Python 3.11.2 (main, Feb 16 2023, 02:55:59) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from scapy.layers.tls.crypto.hkdf import TLS13_HKDF
>>> TLS13_HKDF("sha256")
```

We can also reproduce from the default python docker image:
```
$ docker run -it --entrypoint bash python:latest
# pip install scapy
# python
>>> from scapy.layers.tls.crypto.hkdf import TLS13_HKDF
>>> TLS13_HKDF("sha256")
```

### Actual result

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/homebrew/lib/python3.11/site-packages/scapy/layers/tls/crypto/hkdf.py", line 23, in __init__
    self.hash = _get_hash(hash_name)
                ^^^^^^^^^^^^^^^^^^^^
TypeError: 'NoneType' object is not callable

### Expected result

<scapy.layers.tls.crypto.hkdf.TLS13_HKDF object at 0x...>

### Related resources

_No response_


Discussion:
Looks like the `cryptography` module isn't installed. It can be installed manually or with something like `pip install scapy[all]`. (`all` was introduced recently. `2.5.0` can be installed along with `cryptography` and some other modules using `pip install scapy[complete]`)
Thank you! This worked for me in `zsh`:
```
pip install 'scapy[complete]'
```

I can close this out. I suppose a good followup action would be to detect when modules are missing and inform users in the error message. Would you like me to file a separate issue for that?
> I suppose a good followup action would be to detect when modules are missing and inform users in the error message. Would you like me to file a separate issue for that?

I'm not sure. I think this issue can just be reopened and repurposed to cover this scenario but I'd wait for the maintainers to weigh in here.
@lukevalenta thanks for your report. This is indeed a bug and TLS13_HKDF() should no crash when the cryptography module is not available. Do you feel like providing a fix?
From a quick look, it looks like the Crypto module was made a soft dependency with https://github.com/secdev/scapy/commit/3e96a13b329e704726f3bcce99b91450e34c1848 and with https://github.com/secdev/scapy/commit/e758a218ea1a4017ff4070186da63e1bbb25e710.

It does also say that 'cryptography' is an optional dependency for TLS at https://github.com/secdev/scapy/blob/master/doc/scapy/installation.rst#optional-dependencies.

Looking at some other optional dependencies from that link, like `trace3D()` for `traceroute`, the error message looks as follows:
```
>>> from scapy.layers.inet import traceroute
>>> a,u=traceroute(["www.python.org", "google.com","slashdot.org"])
....
>>> a.trace3D()
...
ModuleNotFoundError: No module named 'vpython'

``` 

So in this case, I suppose the desired behavior here would be to fail with an error like this:
```
>>> from scapy.layers.tls.crypto.hkdf import TLS13_HKDF
>>> TLS13_HKDF()
...
ModuleNotFoundError: No module named 'cryptography'
```

Do you agree?

I'm not sure if I have the time to dig in much further at the moment, but will post back here later if I'm able to commit to making the fix. I'll add 'contributions wanted' to the title following https://github.com/secdev/scapy/blob/master/CONTRIBUTING.md for now. Thanks!
Currently, the error message is displayed at https://github.com/secdev/scapy/blob/master/scapy/layers/tls/__init__.py#L94

I agree that Scapy should not crash here. One possible fix could be to make this get_hash implementation always return KeyError at https://github.com/secdev/scapy/blob/master/scapy/layers/tls/crypto/pkcs1.py and catch the exception at https://github.com/secdev/scapy/blob/master/scapy/layers/tls/crypto/hkdf.py#L24
What about adding the `@crypto_validator` decorator (https://github.com/secdev/scapy/blob/7f89ce51c09417c2aa9d78d3e5c75091cacd4bff/scapy/config.py#L954) to the HKDF  methods?

That seems to be the approach used by the `_EncryptAndVerifyRSA.encrypt` function at https://github.com/secdev/scapy/blob/master/scapy/layers/tls/crypto/pkcs1.py#L140, which also uses the `_get_hash` function.

```
>>> from scapy.layers.tls.crypto.pkcs1 import *
>>> PubKeyRSA.encrypt(None, None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.11/site-packages/scapy/layers/tls/cert.py", line 306, in encrypt
    return _EncryptAndVerifyRSA.encrypt(self, msg, t=t, h=h, mgf=mgf, L=L)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/scapy/config.py", line 951, in func_in
    raise ImportError("Cannot execute crypto-related method! "
ImportError: Cannot execute crypto-related method! Please install python-cryptography v1.7 or later.
```

If you agree with this approach, I'm happy to submit a PR.
Indeed, this seems like a better solution =)

Resolving patch:
diff --git a/scapy/layers/tls/crypto/hkdf.py b/scapy/layers/tls/crypto/hkdf.py
--- a/scapy/layers/tls/crypto/hkdf.py
+++ b/scapy/layers/tls/crypto/hkdf.py
@@ -9,7 +9,7 @@
 
 import struct
 
-from scapy.config import conf
+from scapy.config import conf, crypto_validator
 from scapy.layers.tls.crypto.pkcs1 import _get_hash
 
 if conf.crypto_valid:
@@ -20,9 +20,11 @@
 
 
 class TLS13_HKDF(object):
+    @crypto_validator
     def __init__(self, hash_name="sha256"):
         self.hash = _get_hash(hash_name)
 
+    @crypto_validator
     def extract(self, salt, ikm):
         h = self.hash
         hkdf = HKDF(h, h.digest_size, salt, None, default_backend())
@@ -30,11 +32,13 @@ def extract(self, salt, ikm):
             ikm = b"\x00" * h.digest_size
         return hkdf._extract(ikm)
 
+    @crypto_validator
     def expand(self, prk, info, L):
         h = self.hash
         hkdf = HKDFExpand(h, L, info, default_backend())
         return hkdf.derive(prk)
 
+    @crypto_validator
     def expand_label(self, secret, label, hash_value, length):
         hkdf_label = struct.pack("!H", length)
         hkdf_label += struct.pack("B", 6 + len(label))
@@ -44,6 +48,7 @@ def expand_label(self, secret, label, hash_value, length):
         hkdf_label += hash_value
         return self.expand(secret, hkdf_label, length)
 
+    @crypto_validator
     def derive_secret(self, secret, label, messages):
         h = Hash(self.hash, backend=default_backend())
         h.update(messages)
@@ -51,6 +56,7 @@ def derive_secret(self, secret, label, messages):
         hash_len = self.hash.digest_size
         return self.expand_label(secret, label, hash_messages, hash_len)
 
+    @crypto_validator
     def compute_verify_data(self, basekey, handshake_context):
         hash_len = self.hash.digest_size
         finished_key = self.expand_label(basekey, b"finished", b"", hash_len)


Test patch: