Repository: pallets/werkzeug

Issue:
8 tests fail on a FIPS enabled system
When using werkzeug on a linux system with FIPS enabled, following tests fail:

```console
FAILED tests/test_debug.py::TestDebugHelpers::test_exc_divider_found_on_chained_exception - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_debug.py::test_basic[True] - assert 'The debugger caught an exception in your WSGI application' in '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>500 Internal Server Error</title>\n<h1>Internal Serve...
FAILED tests/test_http.py::TestHTTPUtility::test_is_resource_modified - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_http.py::TestHTTPUtility::test_is_resource_modified_for_range_requests - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_security.py::test_password_hashing - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_wrappers.py::test_etag_response_mixin - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_wrappers.py::test_etag_response_412 - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_wrappers.py::test_etag_response_mixin_freezing - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
```
These tests have been run after cloning this project.

Environment:

- Python version: 3.8.0
- Werkzeug version: [master](https://github.com/pallets/werkzeug/tree/90053efed3b5800b41fe6da89119247241eaa958) branch

The root cause for failure of these tests appear to be MD5, as it does not appear in the list of [approved hashing algorithms](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf) (SHA-1, SHA-224, SHA-256, SHA-384, SHA512, SHA-512/224 and SHA-512/256). 

Is it a possibility to migrate to SHA1 for hashing? 

```python
============================================================================================================ test session starts =============================================================================================================
platform linux -- Python 3.8.0, pytest-5.4.3, py-1.8.1, pluggy-0.13.1
rootdir: /home/ec2-user/werkzeug, inifile: setup.cfg, testpaths: tests
plugins: timeout-1.4.2, xprocess-0.13.1
collected 660 items

tests/test_datastructures.py ...................................................................................................                                                                                                       [ 15%]
tests/test_debug.py ..............F....F....                                                                                                                                                                                           [ 18%]
tests/test_exceptions.py .......................................................                                                                                                                                                       [ 26%]
tests/test_formparser.py ...........................                                                                                                                                                                                   [ 31%]
tests/test_http.py .........................FF..........................                                                                                                                                                               [ 39%]
tests/test_internal.py ...                                                                                                                                                                                                             [ 39%]
tests/test_local.py ..........                                                                                                                                                                                                         [ 41%]
tests/test_routing.py ......................................................................                                                                                                                                           [ 51%]
tests/test_security.py ..F...                                                                                                                                                                                                          [ 52%]
tests/test_send_file.py .........................                                                                                                                                                                                      [ 56%]
tests/test_serving.py .....................                                                                                                                                                                                            [ 59%]
tests/test_test.py ......................................................                                                                                                                                                              [ 67%]
tests/test_urls.py ...........................................                                                                                                                                                                         [ 74%]
tests/test_useragents.py ...                                                                                                                                                                                                           [ 74%]
tests/test_utils.py .................                                                                                                                                                                                                  [ 77%]
tests/test_wrappers.py ....................................FF......F..............................................                                                                                                                     [ 91%]
tests/test_wsgi.py ................................                                                                                                                                                                                    [ 95%]
tests/middleware/test_dispatcher.py .                                                                                                                                                                                                  [ 96%]
tests/middleware/test_http_proxy.py .                                                                                                                                                                                                  [ 96%]
tests/middleware/test_lint.py .........                                                                                                                                                                                                [ 97%]
tests/middleware/test_proxy_fix.py ..............                                                                                                                                                                                      [ 99%]
tests/middleware/test_shared_data.py ..                                                                                                                                                                                                [100%]

================================================================================================================== FAILURES ==================================================================================================================
________________________________________________________________________________________ TestDebugHelpers.test_exc_divider_found_on_chained_exception ________________________________________________________________________________________

request = <Request 'http://localhost/' [GET]>

    @Request.application
    def app(request):
        def do_something():
            raise ValueError("inner")

        try:
>           do_something()

tests/test_debug.py:233:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def do_something():
>       raise ValueError("inner")
E       ValueError: inner

tests/test_debug.py:230: ValueError

During handling of the above exception, another exception occurred:

self = <werkzeug.debug.DebuggedApplication object at 0x7f6ceb2e4250>, environ = {'HTTP_HOST': 'localhost', 'PATH_INFO': '/', 'QUERY_STRING': '', 'RAW_URI': '/', ...}
start_response = <function run_wsgi_app.<locals>.start_response at 0x7f6cebe9b160>

    def debug_application(self, environ, start_response):
        """Run the application and conserve the traceback frames."""
        app_iter = None
        try:
>           app_iter = self.app(environ, start_response)

src/werkzeug/debug/__init__.py:289:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = ({'HTTP_HOST': 'localhost', 'PATH_INFO': '/', 'QUERY_STRING': '', 'RAW_URI': '/', ...}, <function run_wsgi_app.<locals>.start_response at 0x7f6cebe9b160>), request = <Request 'http://localhost/' [GET]>

    def application(*args):
        request = cls(args[-2])
        with request:
            try:
>               resp = f(*args[:-2] + (request,))

src/werkzeug/wrappers/base_request.py:236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

request = <Request 'http://localhost/' [GET]>

    @Request.application
    def app(request):
        def do_something():
            raise ValueError("inner")

        try:
            do_something()
        except ValueError:
>           raise KeyError("outer")
E           KeyError: 'outer'

tests/test_debug.py:235: KeyError

During handling of the above exception, another exception occurred:

self = <test_debug.TestDebugHelpers object at 0x7f6cec9beb50>

    def test_exc_divider_found_on_chained_exception(self):
        @Request.application
        def app(request):
            def do_something():
                raise ValueError("inner")

            try:
                do_something()
            except ValueError:
                raise KeyError("outer")

        debugged = DebuggedApplication(app)
        client = Client(debugged, Response)
>       response = client.get("/")

tests/test_debug.py:239:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/werkzeug/test.py:995: in get
    return self.open(*args, **kw)
src/werkzeug/test.py:959: in open
    response = self.run_wsgi_app(environ.copy(), buffered=buffered)
src/werkzeug/test.py:850: in run_wsgi_app
    rv = run_wsgi_app(self.application, environ, buffered=buffered)
src/werkzeug/test.py:1106: in run_wsgi_app
    for item in app_iter:
src/werkzeug/debug/__init__.py:326: in debug_application
    is_trusted = bool(self.check_pin_trust(environ))
src/werkzeug/debug/__init__.py:370: in check_pin_trust
    if self.pin is None:
src/werkzeug/debug/__init__.py:271: in pin
    self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

app = <function TestDebugHelpers.test_exc_divider_found_on_chained_exception.<locals>.app at 0x7f6cebe9b5e0>

    def get_pin_and_cookie_name(app):
        """Given an application object this returns a semi-stable 9 digit pin
        code and a random key.  The hope is that this is stable between
        restarts to not make debugging particularly frustrating.  If the pin
        was forcefully disabled this returns `None`.

        Second item in the resulting tuple is the cookie name for remembering.
        """
        pin = os.environ.get("WERKZEUG_DEBUG_PIN")
        rv = None
        num = None

        # Pin was explicitly disabled
        if pin == "off":
            return None, None

        # Pin was provided explicitly
        if pin is not None and pin.replace("-", "").isdigit():
            # If there are separators in the pin, return it directly
            if "-" in pin:
                rv = pin
            else:
                num = pin

        modname = getattr(app, "__module__", app.__class__.__module__)

        try:
            # getuser imports the pwd module, which does not exist in Google
            # App Engine. It may also raise a KeyError if the UID does not
            # have a username, such as in Docker.
            username = getpass.getuser()
        except (ImportError, KeyError):
            username = None

        mod = sys.modules.get(modname)

        # This information only exists to make the cookie unique on the
        # computer, not as a security feature.
        probably_public_bits = [
            username,
            modname,
            getattr(app, "__name__", type(app).__name__),
            getattr(mod, "__file__", None),
        ]

        # This information is here to make it harder for an attacker to
        # guess the cookie name.  They are unlikely to be contained anywhere
        # within the unauthenticated debug page.
        private_bits = [str(uuid.getnode()), get_machine_id()]

>       h = hashlib.md5()
E       ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips

src/werkzeug/debug/__init__.py:172: ValueError
______________________________________________________________________________________________________________ test_basic[True] ______________________________________________________________________________________________________________

dev_server = <function dev_server.<locals>.run_dev_server at 0x7f6cee774e50>, crash = True

    @pytest.mark.parametrize("crash", (True, False))
    def test_basic(dev_server, crash):
        server = dev_server(
            f"""
            from werkzeug.debug import DebuggedApplication

            @DebuggedApplication
            def app(environ, start_response):
                if {crash}:
                    1 / 0
                start_response('200 OK', [('Content-Type', 'text/html')])
                return [b'hello']
            """
        )

        r = requests.get(server.url)
        assert r.status_code == 500 if crash else 200
        if crash:
>           assert "The debugger caught an exception in your WSGI application" in r.text
E           assert 'The debugger caught an exception in your WSGI application' in '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>500 Internal Server Error</title>\n<h1>Internal Serve...nd was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>\n'
E            +  where '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>500 Internal Server Error</title>\n<h1>Internal Serve...nd was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>\n' = <Response [500]>.text

tests/test_debug.py:325: AssertionError
------------------------------------------------------------------------------------------------------------ Captured stdout call ------------------------------------------------------------------------------------------------------------
/home/ec2-user/werkzeug/.xprocess/dev_server$ /home/ec2-user/werkzeug/env/bin/python3 /home/ec2-user/werkzeug/tests/conftest.py /tmp/pytest-of-ec2-user/pytest-0/test_basic_True_0
process 'dev_server' started pid=9845
 * Running on http://localhost:13220/ (Press CTRL+C to quit)

pid=9845

dev_server process startup detected
------------------------------------------------------------------------------------------------------------ Captured stderr call ------------------------------------------------------------------------------------------------------------
Exception ignored in: <function Popen.__del__ at 0x7f6ceec6ae50>
Traceback (most recent call last):
  File "/opt/rh/rh-python38/root/usr/lib64/python3.8/subprocess.py", line 942, in __del__
    _warn("subprocess %s is still running" % self.pid,
ResourceWarning: subprocess 9845 is still running
_________________________________________________________________________________________________ TestHTTPUtility.test_is_resource_modified __________________________________________________________________________________________________

self = <test_http.TestHTTPUtility object at 0x7f6ceb9589d0>

    def test_is_resource_modified(self):
        env = create_environ()

        # any method is allowed
        env["REQUEST_METHOD"] = "POST"
        assert http.is_resource_modified(env, etag="testing")
        env["REQUEST_METHOD"] = "GET"

        # etagify from data
        pytest.raises(TypeError, http.is_resource_modified, env, data="42", etag="23")
>       env["HTTP_IF_NONE_MATCH"] = http.generate_etag(b"awesome")

tests/test_http.py:390:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = b'awesome'

    def generate_etag(data):
        """Generate an etag for some data."""
>       return md5(data).hexdigest()
E       ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips

src/werkzeug/http.py:797: ValueError
________________________________________________________________________________________ TestHTTPUtility.test_is_resource_modified_for_range_requests ________________________________________________________________________________________

self = <test_http.TestHTTPUtility object at 0x7f6ceb977d90>

    def test_is_resource_modified_for_range_requests(self):
        env = create_environ()

        env["HTTP_IF_MODIFIED_SINCE"] = http.http_date(datetime(2008, 1, 1, 12, 30))
>       env["HTTP_IF_RANGE"] = http.generate_etag(b"awesome_if_range")

tests/test_http.py:405:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = b'awesome_if_range'

    def generate_etag(data):
        """Generate an etag for some data."""
>       return md5(data).hexdigest()
E       ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips

src/werkzeug/http.py:797: ValueError
___________________________________________________________________________________________________________ test_password_hashing ____________________________________________________________________________________________________________

    def test_password_hashing():
        hash0 = generate_password_hash("default")
        assert check_password_hash(hash0, "default")
        assert hash0.startswith("pbkdf2:sha256:150000$")

        hash1 = generate_password_hash("default", "sha1")
        hash2 = generate_password_hash("default", method="sha1")
        assert hash1 != hash2
        assert check_password_hash(hash1, "default")
        assert check_password_hash(hash2, "default")
        assert hash1.startswith("sha1$")
        assert hash2.startswith("sha1$")

        with pytest.raises(ValueError):
            check_password_hash("$made$up$", "default")

        with pytest.raises(ValueError):
            generate_password_hash("default", "sha1", salt_length=0)

        fakehash = generate_password_hash("default", method="plain")
        assert fakehash == "plain$$default"
        assert check_password_hash(fakehash, "default")

>       mhash = generate_password_hash("default", method="md5")

tests/test_security.py:58:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/werkzeug/security.py:181: in generate_password_hash
    h, actual_method = _hash_internal(method, salt, password)
src/werkzeug/security.py:138: in _hash_internal
    mac = _create_mac(salt, password, method)
src/werkzeug/security.py:152: in _create_mac
    return hmac.HMAC(key, msg, hashfunc)
/opt/rh/rh-python38/root/usr/lib64/python3.8/hmac.py:162: in __new__
    name = _get_openssl_name(digestmod)
/opt/rh/rh-python38/root/usr/lib64/python3.8/hmac.py:149: in _get_openssl_name
    digestmod = digestmod(b'')
src/werkzeug/security.py:150: in hashfunc
    return hashlib.new(method, d)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

name = 'md5', data = b'', kwargs = {}

    def __hash_new(name, data=b'', **kwargs):
        """new(name, data=b'') - Return a new hashing object using the named algorithm;
        optionally initialized with data (which must be a bytes-like object).
        """
        if not _hashlib.get_fips_mode():
            if name in __block_openssl_constructor:
                # Prefer our blake2 and sha3 implementation
                # OpenSSL 1.1.0 comes with a limited implementation of blake2b/s.
                # It does neither support keyed blake2 nor advanced features like
                # salt, personal, tree hashing or SSE.
                return __get_builtin_constructor(name)(data, **kwargs)
        try:
>           return _hashlib.new(name, data, **kwargs)
E           ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips

/opt/rh/rh-python38/root/usr/lib64/python3.8/hashlib.py:169: ValueError
__________________________________________________________________________________________________________ test_etag_response_mixin __________________________________________________________________________________________________________

    def test_etag_response_mixin():
        response = wrappers.Response("Hello World")
        assert response.get_etag() == (None, None)
>       response.add_etag()

tests/test_wrappers.py:747:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/werkzeug/wrappers/etag.py:241: in add_etag
    self.set_etag(generate_etag(self.get_data()), weak)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = b'Hello World'

    def generate_etag(data):
        """Generate an etag for some data."""
>       return md5(data).hexdigest()
E       ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips

src/werkzeug/http.py:797: ValueError
___________________________________________________________________________________________________________ test_etag_response_412 ___________________________________________________________________________________________________________

    def test_etag_response_412():
        response = wrappers.Response("Hello World")
        assert response.get_etag() == (None, None)
>       response.add_etag()

tests/test_wrappers.py:788:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/werkzeug/wrappers/etag.py:241: in add_etag
    self.set_etag(generate_etag(self.get_data()), weak)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = b'Hello World'

    def generate_etag(data):
        """Generate an etag for some data."""
>       return md5(data).hexdigest()
E       ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips

src/werkzeug/http.py:797: ValueError
_____________________________________________________________________________________________________ test_etag_response_mixin_freezing ______________________________________________________________________________________________________

    def test_etag_response_mixin_freezing():
        class WithFreeze(wrappers.ETagResponseMixin, wrappers.BaseResponse):
            pass

        class WithoutFreeze(  # type: ignore
            wrappers.BaseResponse, wrappers.ETagResponseMixin
        ):
            pass

        response = WithFreeze("Hello World")
>       response.freeze()

tests/test_wrappers.py:917:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/werkzeug/wrappers/etag.py:259: in freeze
    self.add_etag()
src/werkzeug/wrappers/etag.py:241: in add_etag
    self.set_etag(generate_etag(self.get_data()), weak)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = b'Hello World'

    def generate_etag(data):
        """Generate an etag for some data."""
>       return md5(data).hexdigest()
E       ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips

src/werkzeug/http.py:797: ValueError
========================================================================================================== short test summary info ===========================================================================================================
FAILED tests/test_debug.py::TestDebugHelpers::test_exc_divider_found_on_chained_exception - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_debug.py::test_basic[True] - assert 'The debugger caught an exception in your WSGI application' in '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>500 Internal Server Error</title>\n<h1>Internal Serve...
FAILED tests/test_http.py::TestHTTPUtility::test_is_resource_modified - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_http.py::TestHTTPUtility::test_is_resource_modified_for_range_requests - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_security.py::test_password_hashing - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_wrappers.py::test_etag_response_mixin - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_wrappers.py::test_etag_response_412 - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
FAILED tests/test_wrappers.py::test_etag_response_mixin_freezing - ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for fips
================================================================================================== 8 failed, 652 passed in 98.71s (0:01:38) ==================================================================================================
```


Discussion:
Although using MD5 in this case isn't a security problem and should generally work, in my opinion also it would be better to migrate it. Changing the hash function will improve a bit the quality of Etag and shouldn't introduce any significant compatibility problems.
In reality MD5 and SHA1 both have security vulnerabilities. Please see 

- MD5 [Wikipedia Page](https://en.wikipedia.org/wiki/MD5#cite_note-3)
- [Google Security Blog](https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html)
Of course, probably it would be better to choose a modern hash. Anyway, this place it's not a secure risky - but of course, still better remove usage of MD5/SHA1

Resolving patch:
diff --git a/src/werkzeug/debug/__init__.py b/src/werkzeug/debug/__init__.py
--- a/src/werkzeug/debug/__init__.py
+++ b/src/werkzeug/debug/__init__.py
@@ -32,7 +32,7 @@
 
 
 def hash_pin(pin: str) -> str:
-    return hashlib.md5(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
+    return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
 
 
 _machine_id: t.Optional[str] = None
@@ -176,7 +176,7 @@ def get_pin_and_cookie_name(
     # within the unauthenticated debug page.
     private_bits = [str(uuid.getnode()), get_machine_id()]
 
-    h = hashlib.md5()
+    h = hashlib.sha1()
     for bit in chain(probably_public_bits, private_bits):
         if not bit:
             continue
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -6,7 +6,7 @@
 from datetime import datetime
 from datetime import timedelta
 from email.utils import parsedate_tz
-from hashlib import md5
+from hashlib import sha1
 from time import gmtime
 from time import struct_time
 from time import time
@@ -867,8 +867,12 @@ def parse_etags(value: t.Optional[str]) -> "ds.ETags":
 
 
 def generate_etag(data: bytes) -> str:
-    """Generate an etag for some data."""
-    return md5(data).hexdigest()
+    """Generate an etag for some data.
+
+    .. versionchanged:: 2.0
+        Use SHA-1. MD5 may not be available in some environments.
+    """
+    return sha1(data).hexdigest()
 
 
 def parse_date(value: t.Optional[str]) -> t.Optional[datetime]:
@@ -1025,6 +1029,10 @@ def is_resource_modified(
                             account.
     :return: `True` if the resource was modified, otherwise `False`.
 
+    .. versionchanged:: 2.0
+        SHA-1 is used to generate an etag value for the data. MD5 may
+        not be available in some environments.
+
     .. versionchanged:: 1.0.0
         The check is run for methods other than ``GET`` and ``HEAD``.
     """
diff --git a/src/werkzeug/wrappers/response.py b/src/werkzeug/wrappers/response.py
--- a/src/werkzeug/wrappers/response.py
+++ b/src/werkzeug/wrappers/response.py
@@ -1229,7 +1229,12 @@ def make_conditional(
         return self
 
     def add_etag(self, overwrite: bool = False, weak: bool = False) -> None:
-        """Add an etag for the current response if there is none yet."""
+        """Add an etag for the current response if there is none yet.
+
+        .. versionchanged:: 2.0
+            SHA-1 is used to generate the value. MD5 may not be
+            available in some environments.
+        """
         if overwrite or "etag" not in self.headers:
             self.set_etag(generate_etag(self.get_data()), weak)
 


Test patch:
diff --git a/tests/test_security.py b/tests/test_security.py
--- a/tests/test_security.py
+++ b/tests/test_security.py
@@ -43,16 +43,6 @@ def test_password_hashing():
     assert fakehash == "plain$$default"
     assert check_password_hash(fakehash, "default")
 
-    mhash = generate_password_hash("default", method="md5")
-    assert mhash.startswith("md5$")
-    assert check_password_hash(mhash, "default")
-
-    legacy = "md5$$c21f969b5f03d33d43e04f8f136e7682"
-    assert check_password_hash(legacy, "default")
-
-    legacy = "md5$$c21f969b5f03d33d43e04f8f136e7682"
-    assert check_password_hash(legacy, "default")
-
 
 def test_safe_join():
     assert safe_join("foo", "bar/baz") == posixpath.join("foo", "bar/baz")
diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers.py
@@ -700,7 +700,7 @@ def test_etag_response():
     response = wrappers.Response("Hello World")
     assert response.get_etag() == (None, None)
     response.add_etag()
-    assert response.get_etag() == ("b10a8db164e0754105b7a99be72e3fe5", False)
+    assert response.get_etag() == ("0a4d55a8d778e5022fab701977c5d840bbc486d0", False)
     assert not response.cache_control
     response.cache_control.must_revalidate = True
     response.cache_control.max_age = 60
@@ -741,7 +741,7 @@ def test_etag_response_412():
     response = wrappers.Response("Hello World")
     assert response.get_etag() == (None, None)
     response.add_etag()
-    assert response.get_etag() == ("b10a8db164e0754105b7a99be72e3fe5", False)
+    assert response.get_etag() == ("0a4d55a8d778e5022fab701977c5d840bbc486d0", False)
     assert not response.cache_control
     response.cache_control.must_revalidate = True
     response.cache_control.max_age = 60