Skip to content

gh-99108: Implement HACL* HMAC #130157

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

Merged
merged 55 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
20ce770
Merge branch 'main' into feat/hmac/hacl-99108
picnixz Feb 15, 2025
713fe1b
Merge remote-tracking branch 'upstream/main' into feat/hmac/hacl-99108
picnixz Mar 11, 2025
bc2a2f8
Merge remote-tracking branch 'upstream/main' into feat/hmac/hacl-99108
picnixz Mar 12, 2025
9fc5a71
Merge remote-tracking branch 'upstream/main' into feat/hmac/hacl-99108
picnixz Mar 16, 2025
e88b113
Merge remote-tracking branch 'upstream/main' into feat/hmac/hacl-99108
picnixz Mar 17, 2025
d2e72aa
update vendored HACL* project
picnixz Dec 12, 2024
9fa2b0b
prepare build configurations
picnixz Dec 12, 2024
1f33995
add HMAC module
picnixz Dec 13, 2024
8162616
add HMAC module state
picnixz Dec 13, 2024
02988f0
add HMAC-HASH static information
picnixz Dec 13, 2024
8df0fbf
implement HMAC-HASH known hashes table
picnixz Dec 13, 2024
f541600
add HMAC unknown hash exception type
picnixz Dec 13, 2024
88d2b96
intern `"lower"` for `str.lower` calls
picnixz Dec 13, 2024
77e51d4
implement hash information lookup
picnixz Dec 13, 2024
264e43c
add one-shot HMAC HACL* minimal API
picnixz Dec 13, 2024
c791feb
implement one-shot HMAC
picnixz Dec 13, 2024
6b77844
implement minimal HMAC object interface
picnixz Dec 13, 2024
0770202
implement HMAC simple getters
picnixz Dec 13, 2024
af443c2
implement `HMAC.__repr__()` method
picnixz Dec 12, 2024
4a0e9cd
implement streaming `HMAC.update()` macros
picnixz Dec 12, 2024
2d02577
implement streaming `HMAC.update()` interface
picnixz Dec 12, 2024
2b010ef
implement `HMAC.{digest,hexdigest}()` methods
picnixz Dec 12, 2024
6227892
implement `HMAC.copy()` interface
picnixz Dec 12, 2024
5c13316
add vectorized blake2s/2b kinds
picnixz Dec 12, 2024
617b793
implement `HMAC.new()` interface
picnixz Dec 12, 2024
cc28e3e
update HMAC python interface
picnixz Dec 12, 2024
166e7e8
add blurb & CHANGELOG entries
picnixz Feb 15, 2025
f2817ae
add `hashlib_helper.requires_builtin_hmac`
picnixz Mar 11, 2025
2b544e5
add various tests for the HACL* HMAC implementation
picnixz Mar 11, 2025
b4bfe5e
update comments concerning `int` return values
picnixz Mar 17, 2025
3aae0a8
handle error code returned by `Hacl_Streaming_HMAC_digest`
picnixz Mar 17, 2025
59b365a
Merge remote-tracking branch 'upstream/main' into feat/hmac/hacl-99108
picnixz Mar 18, 2025
c86d51e
summarize HMAC changes in "Improved Modules" as well
picnixz Mar 18, 2025
5fd72f0
force safe downcast to `uint32_t` in `hmac_new_initial_state`
picnixz Mar 18, 2025
da71ef8
`make clinic`
picnixz Mar 18, 2025
5baa874
fix HMAC max block size
picnixz Mar 18, 2025
10e1189
Merge remote-tracking branch 'upstream/main' into feat/hmac/hacl-99108
picnixz Mar 18, 2025
aab2b0b
align naming of one-shot HMAC with OpenSSL
picnixz Mar 18, 2025
da3a3e3
test one-shot HMAC functions
picnixz Mar 18, 2025
b2540ef
remove dubious `eval`
picnixz Mar 19, 2025
e90f685
Merge remote-tracking branch 'upstream/main' into feat/hmac/hacl-99108
picnixz Mar 23, 2025
0b18234
fixup test docstring
picnixz Mar 28, 2025
218015a
remove un-necessary guards
picnixz Mar 28, 2025
d0ca0e5
Merge remote-tracking branch 'upstream/main' into feat/hmac/hacl-99108
picnixz Mar 28, 2025
0ae4a9d
make clinic
picnixz Mar 28, 2025
47a15d6
make regen-sbom
picnixz Mar 28, 2025
f203c6d
fix obvious typo
picnixz Mar 28, 2025
a5fc810
narrow `except` guard
picnixz Mar 29, 2025
b58073d
put `hmac` section before `http` section
picnixz Mar 29, 2025
6f84a82
cosmetic changes in hmac.py
picnixz Apr 1, 2025
de7ad74
group cryptographic primitives modules
picnixz Apr 1, 2025
674af07
better group cryptographic primitives
picnixz Apr 1, 2025
26cfeb0
update naming
picnixz Apr 1, 2025
258aa20
update naming
picnixz Apr 1, 2025
629cbce
Merge branch 'main' into feat/hmac/hacl-99108
picnixz Apr 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,13 @@ Other language changes
The testbed can also be used to run the test suite of projects other than
CPython itself. (Contributed by Russell Keith-Magee in :gh:`127592`.)

* Add a built-in implementation for HMAC (:rfc:`2104`) using formally verified
code from the `HACL* <https://github.com/hacl-star/hacl-star/>`__ project.
This implementation is used as a fallback when the OpenSSL implementation
of HMAC is not available.
(Contributed by Bénédikt Tran in :gh:`99108`.)


.. _whatsnew314-pep765:

PEP 765: Disallow return/break/continue that exit a finally block
Expand All @@ -464,6 +471,7 @@ The compiler emits a :exc:`SyntaxWarning` when a :keyword:`return`, :keyword:`br
:keyword:`continue` statements appears where it exits a :keyword:`finally` block.
This change is specified in :pep:`765`.


New modules
===========

Expand Down Expand Up @@ -705,6 +713,14 @@ graphlib
(Contributed by Daniel Pope in :gh:`130914`)


hmac
----

* Add a built-in implementation for HMAC (:rfc:`2104`) using formally verified
code from the `HACL* <https://github.com/hacl-star/hacl-star/>`__ project.
(Contributed by Bénédikt Tran in :gh:`99108`.)


http
----

Expand Down
96 changes: 66 additions & 30 deletions Lib/hmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
Implements the HMAC algorithm as described by RFC 2104.
"""

import warnings as _warnings
try:
import _hashlib as _hashopenssl
except ImportError:
Expand All @@ -14,7 +13,10 @@
compare_digest = _hashopenssl.compare_digest
_functype = type(_hashopenssl.openssl_sha256) # builtin type

import hashlib as _hashlib
try:
import _hmac
except ImportError:
_hmac = None

trans_5C = bytes((x ^ 0x5C) for x in range(256))
trans_36 = bytes((x ^ 0x36) for x in range(256))
Expand All @@ -24,11 +26,27 @@
digest_size = None


def _get_digest_constructor(digest_like):
if callable(digest_like):
return digest_like
if isinstance(digest_like, str):
def digest_wrapper(d=b''):
import hashlib
return hashlib.new(digest_like, d)
else:
def digest_wrapper(d=b''):
return digest_like.new(d)
return digest_wrapper


class HMAC:
"""RFC 2104 HMAC class. Also complies with RFC 4231.

This supports the API for Cryptographic Hash Functions (PEP 247).
"""

# Note: self.blocksize is the default blocksize; self.block_size
# is effective block size as well as the public API attribute.
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.

__slots__ = (
Expand All @@ -50,32 +68,47 @@ def __init__(self, key, msg=None, digestmod=''):
"""

if not isinstance(key, (bytes, bytearray)):
raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
raise TypeError(f"key: expected bytes or bytearray, "
f"but got {type(key).__name__!r}")

if not digestmod:
raise TypeError("Missing required argument 'digestmod'.")

self.__init(key, msg, digestmod)

def __init(self, key, msg, digestmod):
if _hashopenssl and isinstance(digestmod, (str, _functype)):
try:
self._init_hmac(key, msg, digestmod)
self._init_openssl_hmac(key, msg, digestmod)
return
except _hashopenssl.UnsupportedDigestmodError:
self._init_old(key, msg, digestmod)
else:
self._init_old(key, msg, digestmod)
pass
if _hmac and isinstance(digestmod, str):
try:
self._init_builtin_hmac(key, msg, digestmod)
return
except _hmac.UnknownHashError:
pass
self._init_old(key, msg, digestmod)

def _init_hmac(self, key, msg, digestmod):
def _init_openssl_hmac(self, key, msg, digestmod):
self._hmac = _hashopenssl.hmac_new(key, msg, digestmod=digestmod)
self._inner = self._outer = None # because the slots are defined
self.digest_size = self._hmac.digest_size
self.block_size = self._hmac.block_size

_init_hmac = _init_openssl_hmac # for backward compatibility (if any)

def _init_builtin_hmac(self, key, msg, digestmod):
self._hmac = _hmac.new(key, msg, digestmod=digestmod)
self._inner = self._outer = None # because the slots are defined
self.digest_size = self._hmac.digest_size
self.block_size = self._hmac.block_size

def _init_old(self, key, msg, digestmod):
if callable(digestmod):
digest_cons = digestmod
elif isinstance(digestmod, str):
digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
else:
digest_cons = lambda d=b'': digestmod.new(d)
import warnings

digest_cons = _get_digest_constructor(digestmod)

self._hmac = None
self._outer = digest_cons()
Expand All @@ -85,21 +118,19 @@ def _init_old(self, key, msg, digestmod):
if hasattr(self._inner, 'block_size'):
blocksize = self._inner.block_size
if blocksize < 16:
_warnings.warn('block_size of %d seems too small; using our '
'default of %d.' % (blocksize, self.blocksize),
RuntimeWarning, 2)
warnings.warn(f"block_size of {blocksize} seems too small; "
f"using our default of {self.blocksize}.",
RuntimeWarning, 2)
blocksize = self.blocksize
else:
_warnings.warn('No block_size attribute on given digest object; '
'Assuming %d.' % (self.blocksize),
RuntimeWarning, 2)
warnings.warn("No block_size attribute on given digest object; "
f"Assuming {self.blocksize}.",
RuntimeWarning, 2)
blocksize = self.blocksize

if len(key) > blocksize:
key = digest_cons(key).digest()

# self.blocksize is the default blocksize. self.block_size is
# effective block size as well as the public API attribute.
self.block_size = blocksize

key = key.ljust(blocksize, b'\0')
Expand Down Expand Up @@ -165,6 +196,7 @@ def hexdigest(self):
h = self._current()
return h.hexdigest()


def new(key, msg=None, digestmod=''):
"""Create a new hashing object and return it.

Expand Down Expand Up @@ -194,25 +226,29 @@ def digest(key, msg, digest):
A hashlib constructor returning a new hash object. *OR*
A module supporting PEP 247.
"""
if _hashopenssl is not None and isinstance(digest, (str, _functype)):
if _hashopenssl and isinstance(digest, (str, _functype)):
try:
return _hashopenssl.hmac_digest(key, msg, digest)
except _hashopenssl.UnsupportedDigestmodError:
pass

if callable(digest):
digest_cons = digest
elif isinstance(digest, str):
digest_cons = lambda d=b'': _hashlib.new(digest, d)
else:
digest_cons = lambda d=b'': digest.new(d)
if _hmac and isinstance(digest, str):
try:
return _hmac.compute_digest(key, msg, digest)
except (OverflowError, _hmac.UnknownHashError):
pass

return _compute_digest_fallback(key, msg, digest)


def _compute_digest_fallback(key, msg, digest):
digest_cons = _get_digest_constructor(digest)
inner = digest_cons()
outer = digest_cons()
blocksize = getattr(inner, 'block_size', 64)
if len(key) > blocksize:
key = digest_cons(key).digest()
key = key + b'\x00' * (blocksize - len(key))
key = key.ljust(blocksize, b'\0')
inner.update(key.translate(trans_36))
outer.update(key.translate(trans_5C))
inner.update(msg)
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/support/hashlib_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,20 @@
except ImportError:
_hashlib = None

try:
import _hmac
except ImportError:
_hmac = None


def requires_hashlib():
return unittest.skipIf(_hashlib is None, "requires _hashlib")


def requires_builtin_hmac():
return unittest.skipIf(_hmac is None, "requires _hmac")


def _decorate_func_or_class(func_or_class, decorator_func):
if not isinstance(func_or_class, type):
return decorator_func(func_or_class)
Expand Down
Loading
Loading