Skip to content

fix: add usedforsecurity=False to MD5 hash for Python 3.13/OpenSSL 3+ compatibility#530

Merged
willmurphyscode merged 6 commits intoanchore:mainfrom
jamestexas:fix-md5-openssl-python313
Jan 27, 2026
Merged

fix: add usedforsecurity=False to MD5 hash for Python 3.13/OpenSSL 3+ compatibility#530
willmurphyscode merged 6 commits intoanchore:mainfrom
jamestexas:fix-md5-openssl-python313

Conversation

@jamestexas
Copy link
Copy Markdown
Contributor

@jamestexas jamestexas commented Jan 20, 2026

Fixes UnsupportedDigestmodError when running on Python 3.13 with OpenSSL 3+, which disables MD5 by default.

The MD5 hash is only used for generating content-based match IDs (not cryptography), so adding usedforsecurity=False is the correct fix. This parameter was added in Python 3.9 for exactly this use case.

Also adds a regression test and removes the now-unnecessary # noqa: S324 comment.

Example error:

Example Error
failed to update grype database: quality gate validation failed: quality gate validation failed (schema: v6.1.3, db_id: c7cab3cf-8a25-44c4-b884-009e80726183): failed with 1 exit status 1 : stdout:  : stderr: �[90m0000�[0m �[0mvalidating DB c7cab3cf-8a25-44c4-b884-009e80726183�[0m
�[90m0000�[0m �[0mminimum expected providers present in 'c7cab3cf-8a25-44c4-b884-009e80726183'�[0m
�[90m0000�[0m �[0mwriting config for result set result_set_0�[0m
�[90m0000�[0m �[36mloading result set 'result_set_0' location='data/yardstick/result/sets/result_set_0.json'�[0m
�[90m0000�[0m �[33mresult-set does not exist: [Errno 2] No such file or directory: 'data/yardstick/result/sets/result_set_0.json'�[0m
�[90m0000�[0m �[0mcapturing data result_set=result_set_0�[0m
�[90m0000�[0m �[0mcapturing data for request 1 of 2�[0m
�[90m0000�[0m �[36mcapturing data image=ghcr.io/chainguard-images/scanner-test:latest@sha256:883dff204211becde1bde7f54d135a0a183296ce2fb70d04e9560b2c5767e294 tool=grype@main+import-db=/tmp/grype-db663807301/.grype-db-manager/dbs/c7cab3cf-8a25-44c4-b884-009e80726183/stage/vulnerability-db_v6.1.3_2026-01-20T21:14:09Z_1768943927.tar.zst profile=None�[0m
�[90m0000�[0m �[36mcapturing via run config image=ghcr.io/chainguard-images/scanner-test@sha256:883dff204211becde1bde7f54d135a0a183296ce2fb70d04e9560b2c5767e294 tool=grype[custom-db]@main+import-db=/tmp/grype-db663807301/.grype-db-manager/dbs/c7cab3cf-8a25-44c4-b884-009e80726183/stage/vulnerability-db_v6.1.3_2026-01-20T21:14:09Z_1768943927.tar.zst�[0m
�[90m0000�[0m �[0mUsing grype from GRYPE_EXECUTABLE_PATH: /usr/bin/grype�[0m
�[90m0001�[0m �[0mimporting given (custom) db from '/tmp/grype-db663807301/.grype-db-manager/dbs/c7cab3cf-8a25-44c4-b884-009e80726183/stage/vulnerability-db_v6.1.3_2026-01-20T21:14:09Z_1768943927.tar.zst'�[0m
�[90m0001�[0m �[36mrunning grype with input=ghcr.io/chainguard-images/scanner-test:latest@sha256:883dff204211becde1bde7f54d135a0a183296ce2fb70d04e9560b2c5767e294�[0m
�[90m0003�[0m �[36mparsing grype results�[0m
�[90m0003�[0m �[36mno db location found in results, using system grype DB (not ideal and may cause issues with date filtering)�[0m
�[90m0003�[0m �[36musing system grype DB...�[0m
[0000] ERROR database does not exist
�[90m0003�[0m �[31munable to open grype DB Command '['grype', 'db', 'status']' returned non-zero exit status 1.�[0m
Traceback (most recent call last):
  File "/usr/bin/grype-db-manager", line 7, in <module>
    sys.exit(run())
             ~~~^^
  File "/usr/lib/python3.13/site-packages/grype_db_manager/cli/__init__.py", line 5, in run
    cli()
    ~~~^^
  File "/usr/lib/python3.13/site-packages/click/core.py", line 1485, in __call__
    return self.main(*args, **kwargs)
           ~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/click/core.py", line 1406, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python3.13/site-packages/click/core.py", line 1873, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/click/core.py", line 1873, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/click/core.py", line 1269, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/click/core.py", line 824, in invoke
    return callback(*args, **kwargs)
  File "/usr/lib/python3.13/site-packages/click/decorators.py", line 46, in new_func
    return f(get_current_context().obj, *args, **kwargs)
  File "/usr/lib/python3.13/site-packages/click/decorators.py", line 34, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/usr/lib/python3.13/site-packages/grype_db_manager/cli/db.py", line 137, in validate_db
    _validate_db(ctx, cfg, db_info, images, db_uuid, recapture, force=force)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/grype_db_manager/cli/db.py", line 228, in _validate_db
    db.capture_results(
    ~~~~~~~~~~~~~~~~~~^
        cfg=yardstick_cfg,
        ^^^^^^^^^^^^^^^^^^
    ...<3 lines>...
        root_dir=cfg.data.root,
        ^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/usr/lib/python3.13/site-packages/grype_db_manager/db/validation.py", line 62, in capture_results
    capture.result_set(
    ~~~~~~~~~~~~~~~~~~^
        result_set=result_set,
        ^^^^^^^^^^^^^^^^^^^^^^
        scan_requests=cfg.result_sets[result_set].scan_requests(),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        profiles=cfg.profiles.data,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/usr/lib/python3.13/site-packages/yardstick/capture.py", line 200, in result_set
    scan_config = one(
        scan_request,
        producer_state=producer_data_path,
        profiles=profiles,
    )
  File "/usr/lib/python3.13/site-packages/yardstick/capture.py", line 126, in one
    match_results, raw_json = run_scan(config=scan_config, profile=profile_obj)
                              ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/yardstick/capture.py", line 53, in run_scan
    result = tool.parse(raw_json, config=config)
  File "/usr/lib/python3.13/site-packages/yardstick/tool/grype.py", line 492, in parse
    match = artifact.Match(
        package=pkg,
    ...<2 lines>...
        config=config,
    )
  File "<string>", line 7, in __init__
  File "/usr/lib/python3.13/site-packages/yardstick/artifact.py", line 315, in __post_init__
    match_id = hashlib.md5(  # noqa: S324
               ~~~~~~~~~~~^^^^^^^^^^^^^^^
        json.dumps(identifier, sort_keys=True, cls=DTEncoder).encode(),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ).hexdigest()
    ^
_hashlib.UnsupportedDigestmodError: [digital envelope routines] unsupported

jamestexas and others added 6 commits August 27, 2025 13:10
- Code currently uses timestamp.isoformat(), which >= py-3.11 supports rfc3319 designations with stdlib
- This removes the unused import since the project already pins to >=3.11, < 3.14
- All tests currently pass with this change

Signed-off-by: James Gardner <james.gardner@chainguard.dev>
… compatibility

Fixes UnsupportedDigestmodError when running on Python 3.13 with newer
OpenSSL versions that have MD5 disabled by default for security.

The MD5 hash in Match.__post_init__ is used solely for generating
content-based match IDs for comparison purposes, not for cryptographic
security. Adding usedforsecurity=False allows MD5 to work even when
disabled in OpenSSL's security policy.

This parameter was added in Python 3.9 specifically for non-security
use cases like checksums and content hashing.

Signed-off-by: James Gardner <james.gardner@chainguard.dev>
… compatibility

Fixes UnsupportedDigestmodError when running on Python 3.13 with newer
OpenSSL versions that have MD5 disabled by default for security.

The MD5 hash in Match.__post_init__ is used solely for generating
content-based match IDs for comparison purposes, not for cryptographic
security. Adding usedforsecurity=False allows MD5 to work even when
disabled in OpenSSL's security policy.

This parameter was added in Python 3.9 specifically for non-security
use cases like checksums and content hashing.

Also adds a regression test to ensure Match objects can be created
without raising UnsupportedDigestmodError on systems with strict
OpenSSL configurations.

Removed the now-unnecessary noqa: S324 comment since modern bandit
versions recognize the usedforsecurity parameter.

Signed-off-by: James Gardner <james.gardner@chainguard.dev>
assert expected == le.effective_year(by_cve=year_from_cve_only)


def test_match_md5_hash_with_openssl3():
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're testing the regression (that Match objects can be created without errors), not the MD5 implementation itself here.

@jamestexas jamestexas marked this pull request as ready for review January 20, 2026 22:16
@willmurphyscode willmurphyscode merged commit 553b9fb into anchore:main Jan 27, 2026
3 checks passed
@jamestexas jamestexas deleted the fix-md5-openssl-python313 branch February 3, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants