Skip to content

Cannot save SVG file with FIPS compliant Python #18192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
daytonb opened this issue Aug 6, 2020 · 4 comments · Fixed by #18193
Closed

Cannot save SVG file with FIPS compliant Python #18192

daytonb opened this issue Aug 6, 2020 · 4 comments · Fixed by #18193
Milestone

Comments

@daytonb
Copy link
Contributor

daytonb commented Aug 6, 2020

Bug report

Bug summary

If you use a FIPS (Federal Information Processing Standards) compliant Python install, then you cannot use plt.savefig to save an image in SVG format. This is because the RenderSVG._make_id method takes the first 10 characters of a hashlib.md5 digest of entries in the SVG as the ID for that part of the SVG file. The MD5 algorithm is disabled on FIPS compliant systems or in FIPS compliant Python installs.

Code for reproduction

The big requirement here is a FIPS compliant system or at least a Python install compiled to be FIPS compliant. Then the following code will reproduce the error.

To reproduce,

  1. I started a CentOS 8 virtual machine
  2. Ran sudo fips-mode-setup --enable && sudo reboot
  3. After the reboot I confirmed FIPS mode had been enabled by running fips-mode-setup --check and it returned FIPS mode is enabled..
  4. sudo dnf install python3
  5. python3 -m venv venv
  6. ./venv/bin/python -m pip install -U pip setuptools
  7. ./venv/bin/python -m pip install matplotlib
  8. ./venv/bin/python demo.py. The contents of demo.py are listed below.
""" demo.py: demonstrates that matplotlib can't save SVGs if FIPS mode enabled """
import matplotlib.pyplot as plt
plt.plot(range(10))
plt.savefig(range10.png")
plt.savefig("range10.svg")

Actual outcome

Traceback (most recent call last):
  File "demo.py", line 5, in <module>
    plt.savefig("range10.svg")
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/pyplot.py", line 842, in savefig
    res = fig.savefig(*args, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/figure.py", line 2311, in savefig
    self.canvas.print_figure(fname, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/backend_bases.py", line 2217, in print_figure
    **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/backends/backend_svg.py", line 1318, in print_svg
    self._print_svg(filename, fh, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/backend_bases.py", line 1639, in wrapper
    return func(*args, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/backends/backend_svg.py", line 1342, in _print_svg
    self.figure.draw(renderer)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/artist.py", line 41, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/figure.py", line 1864, in draw
    renderer, self, artists, self.suppressComposite)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/image.py", line 132, in _draw_list_compositing_images
    a.draw(renderer)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/artist.py", line 41, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/cbook/deprecation.py", line 411, in wrapper
    return func(*inner_args, **inner_kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/axes/_base.py", line 2748, in draw
    mimage._draw_list_compositing_images(renderer, self, artists)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/image.py", line 132, in _draw_list_compositing_images
    a.draw(renderer)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/artist.py", line 41, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/axis.py", line 1169, in draw
    tick.draw(renderer)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/artist.py", line 41, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/axis.py", line 291, in draw
    artist.draw(renderer)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/artist.py", line 41, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/lines.py", line 854, in draw
    fc_rgba)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/backends/backend_svg.py", line 683, in draw_markers
    oid = self._make_id('m', dictkey)
  File "/home/daytonb/venv/lib64/python3.6/site-packages/matplotlib/backends/backend_svg.py", line 440, in _make_id
    m = hashlib.md5()
ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS

Expected outcome

Expect no errors and an SVG named "range10.svg" that shows the same plot as a PNG named "range10.png".

Matplotlib version

  • Operating system: CentOS Linux release 8.2.2004 (Core)
    • fips-mode-setup --check returns FIPS mode is enabled
  • Matplotlib version: 3.3.0
  • Matplotlib backend (print(matplotlib.get_backend())): agg
  • Python version: 3.6.8
  • Jupyter version (if applicable): NA
  • Other libraries:
[daytonb@centos-s-1vcpu-1gb-nyc1-01 ~]$ venv/bin/python -m pip list
Package         Version
--------------- -------
cycler          0.10.0
kiwisolver      1.2.0
matplotlib      3.3.0
numpy           1.19.1
Pillow          7.2.0
pip             20.2.1
pyparsing       2.4.7
python-dateutil 2.8.1
setuptools      49.2.1
six             1.15.0
@daytonb
Copy link
Contributor Author

daytonb commented Aug 6, 2020

I'll provide a pull request soon. The Python docs for hashlib includes the following note:

Constructors for hash algorithms that are always present in this module are sha1(), sha224(), sha256(), sha384(), sha512(), blake2b(), and blake2s(). md5() is normally available as well, though it may be missing if you are using a rare “FIPS compliant” build of Python.

I tested that I can just change the hashlib.md5() to hashlib.sha1() or any of the other FIPS secure hash algorithms. It doesn't really matter what hash algorithm we use because we only keep the first 10 characters of the hash anyway to make a unique but reproducible ID for each entry in the SVG file.

[daytonb@centos-s-1vcpu-1gb-nyc1-01 ~]$ diff -u venv{,-patched}/lib/python3.6/site-packages/matplotlib/backends/backe
nd_svg.py
--- venv/lib/python3.6/site-packages/matplotlib/backends/backend_svg.py 2020-08-06 12:07:45.598074913 +0000
+++ venv-patched/lib/python3.6/site-packages/matplotlib/backends/backend_svg.py 2020-08-06 12:33:51.780719116 +0000
@@ -437,7 +437,7 @@
         salt = mpl.rcParams['svg.hashsalt']
         if salt is None:
             salt = str(uuid.uuid4())
-        m = hashlib.md5()
+        m = hashlib.sha1()
         m.update(salt.encode('utf8'))
         m.update(str(content).encode('utf8'))
         return '%s%s' % (type, m.hexdigest()[:10])
[daytonb@centos-s-1vcpu-1gb-nyc1-01 ~]$ venv-patched/bin/python demo.py
[daytonb@centos-s-1vcpu-1gb-nyc1-01 ~]$

daytonb added a commit to daytonb/matplotlib that referenced this issue Aug 6, 2020
@jklymak
Copy link
Member

jklymak commented Aug 6, 2020

Is there any reason to not use the standard library uuid?

@daytonb
Copy link
Contributor Author

daytonb commented Aug 6, 2020

I was actually unaware of the uuid library, which is ironic since the salt for the hash is produced by uuid.uuid4().

Here's my 2 cents on the idea of using the functions for the uuid library based on my reading of the uuid documentation:

  • Not uuid.uuid3 since it uses MD5
  • Maybe uuid.uuid4 since it's random. This does differ, however, from the current behavior of producing the same id if the content passed to _make_id is the same as before. I'm guessing this is an intentional design feature since it allows for 100% reproducible SVGs
    • I think uuid.uuid1 would have the similar benefits and drawbacks for our use case as uuid.uuid4.
  • Maybe uuid.uuid5 since it is based on the content supplied to _make_id. However, I don't know much about the UUID namespaces to determine if one of them is really an appropriate fit for our use case or if we'd just be abusing one of them. On the other hand, maybe the resulting UUID this is exactly what we want out of an ID in the SVG file and we could stop trimming the ID down to 10 characters.

@daytonb
Copy link
Contributor Author

daytonb commented Aug 6, 2020

My pull request #18193 on this has passed tests and is just waiting for review now. I chose to use SHA512 rather than SHA1 which I first tested. I chose SHA512 for future proofing. I know in recent news there has been some research in generating SHA1 collisions which isn't really an issue for matplotlib's use of SHA1, but I can forsee a FIPS-like specification in the future dropping SHA1 like it did MD5.

@dopplershift dopplershift added this to the v3.4.0 milestone Aug 7, 2020
daytonb added a commit to daytonb/matplotlib that referenced this issue Aug 7, 2020
daytonb added a commit to daytonb/matplotlib that referenced this issue Aug 7, 2020
daytonb added a commit to daytonb/matplotlib that referenced this issue Aug 7, 2020
jkseppan added a commit that referenced this issue Aug 7, 2020
Allow savefig to save SVGs on FIPS enabled systems #18192
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants