Skip to content

Commit

Permalink
Rule to check for insufficient key size to HMAC
Browse files Browse the repository at this point in the history
HMAC algorithms require a minimum key size that corresponds to
their digest size. Using a size less than the digest size is
considered weak.

Signed-off-by: Eric Brown <eric.brown@securesauce.dev>
  • Loading branch information
ericwb committed Mar 26, 2024
1 parent 3d06551 commit 48fe9de
Show file tree
Hide file tree
Showing 63 changed files with 502 additions and 45 deletions.
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@
| PY031 | [http — unrestricted bind](rules/python/stdlib/http-server-unrestricted-bind.md) | Binding to an Unrestricted IP Address in `http.server` Module |
| PY032 | [xmlrpc — unrestricted bind](rules/python/stdlib/xmlrpc-server-unrestricted-bind.md) | Binding to an Unrestricted IP Address in `xmlrpc.server` Module |
| PY033 | [re — denial of service](rules/python/stdlib/re-denial-of-service.md) | Inefficient Regular Expression Complexity in `re` Module |
| PY034 | [hmac — weak key](rules/python/stdlib/hmac-weak-key.md) | Insufficient `hmac` Key Size |
10 changes: 10 additions & 0 deletions docs/rules/python/stdlib/hmac-weak-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
id: PY034
title: hmac — weak key
hide_title: true
pagination_prev: null
pagination_next: null
slug: /rules/PY034
---

::: precli.rules.python.stdlib.hmac_weak_key
10 changes: 10 additions & 0 deletions precli/parsers/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,17 @@ def visit_assignment(self, nodes: list[Node]):
):
left_hand = self.resolve(nodes[0], default=nodes[0])
right_hand = self.resolve(nodes[2], default=nodes[2])

# This is in case a variable is reassigned
self.current_symtab.put(
nodes[0].text.decode(), tokens.IDENTIFIER, right_hand
)

# This is to help full resolution of an attribute/call
# TODO: reconsider how this is done. There are two entries for this
# assignment.
self.current_symtab.put(left_hand, tokens.IDENTIFIER, right_hand)

if nodes[2].type == tokens.CALL:
(call_args, call_kwargs) = self.get_func_args(
nodes[2].children[1]
Expand Down
208 changes: 208 additions & 0 deletions precli/rules/python/stdlib/hmac_weak_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Copyright 2024 Secure Saurce LLC
r"""
# Insufficient `hmac` Key Size
This rule identifies instances where the key provided to `hmac.digest()` or
`hmac.new()` is considered too small relative to the digest algorithm's
digest size. Using keys that are too short can compromise the integrity and
security of the HMAC (Hash-based Message Authentication Code), making it less
resistant to brute-force attacks.
HMAC is a mechanism for message authentication using cryptographic hash
functions. The security of an HMAC depends significantly on the secret key's
strength. A key that is shorter than the hash function's output size
(digest size) can reduce the HMAC's effectiveness, making it more vulnerable
to attacks. It is essential to use keys of adequate length to maintain the
expected level of security, especially against brute-force attacks.
Ensure that the key length used with `hmac.digest()` or `hmac.new()` is at
least equal to the digest size of the hash function being used. This
compliance requirement helps maintain the cryptographic strength of the
HMAC and protects the integrity of the message authentication process.
## Example
```python
import hashlib
import hmac
import secrets
key = secrets.token_bytes(None)
message = b"Hello, world!"
hmac.new(key, msg=message, digestmod=hashlib.sha3_384)
```
## Remediation
Adjust the key size to be least the length of the digest size.
```python
import hashlib
import hmac
import secrets
key = secrets.token_bytes(nbytes=48)
message = b"Hello, world!"
hmac.new(key, msg=message, digestmod=hashlib.sha3_384)
```
## See also
- [hmac — Keyed-Hashing for Message Authentication](https://docs.python.org/3/library/hmac.html)
- [secrets — Generate secure random numbers for managing secrets](https://docs.python.org/3/library/secrets.html#generating-tokens)
- [CWE-326: Inadequate Encryption Strength](https://cwe.mitre.org/data/definitions/326.html)
_New in version 0.4.3_
""" # noqa: E501
from precli.core.call import Call
from precli.core.location import Location
from precli.core.result import Result
from precli.rules import Rule


HASH_NAME_SIZES = {
"blake2s": 32,
"blake2b": 64,
"sha224": 28,
"sha256": 32,
"sha384": 48,
"sha512": 64,
"sha3_224": 28,
"sha3_256": 32,
"sha3_384": 48,
"sha3_512": 64,
"sha512_224": 28,
"sha512_256": 32,
"shake_128": 0, # TODO
"shake_256": 0, # TODO
"sm3": 32,
}

HASHLIB_SIZES = {
"hashlib.blake2s": 32,
"hashlib.blake2b": 64,
"hashlib.sha224": 28,
"hashlib.sha256": 32,
"hashlib.sha384": 48,
"hashlib.sha512": 64,
"hashlib.sha3_224": 28,
"hashlib.sha3_256": 32,
"hashlib.sha3_384": 48,
"hashlib.sha3_512": 64,
"hashlib.sha512_224": 28,
"hashlib.sha512_256": 32,
"hashlib.shake_128": 0, # TODO
"hashlib.shake_256": 0, # TODO
"hashlib.sm3": 32,
}


class HmacWeakKey(Rule):
def __init__(self, id: str) -> None:
super().__init__(
id=id,
name="insufficient_key_size",
description=__doc__,
cwe_id=326,
message="The given key is only '{0}' bytes which is insufficient "
"for the '{2}' algorithm.",
wildcards={
"hashlib.*": [
"blake2s",
"blake2b",
"sha224",
"sha256",
"sha384",
"sha512",
"sha3_224",
"sha3_256",
"sha3_384",
"sha3_512",
"sha512_224",
"sha512_256",
"shake_128",
"shake_256",
"sm3",
],
"hmac.*": [
"new",
"digest",
],
},
)

def analyze_call(self, context: dict, call: Call) -> Result:
if call.name_qualified not in ("hmac.new", "hmac.digest"):
return

# new(key, msg=None, digestmod='')
# Create a new hashing object and return it.
#
# key: bytes or buffer, The starting key for the hash.
# msg: bytes or buffer, Initial input for the hash, or None.
# digestmod: A hash name suitable for hashlib.new(). *OR*
# A hashlib constructor returning a new hash object.
# *OR*
# A module supporting PEP 247.
#
# Required as of 3.8, despite its position after the
# optional
# msg argument. Passing it as a keyword argument is
# recommended, though not required for legacy API
# reasons.
#
# You can now feed arbitrary bytes into the object using its
# update() method, and can ask for the hash value at any time
# by calling its digest() or hexdigest() methods.
#
# digest(key, msg, digest)
# Fast inline implementation of HMAC.
#
# key: bytes or buffer, The key for the keyed hash object.
# msg: bytes or buffer, Input message.
# digest: A hash name suitable for hashlib.new() for best
# performance. *OR*
# A hashlib constructor returning a new hash object.
# *OR*
# A module supporting PEP 247.
if call.name_qualified == "hmac.new":
arg_name = "digestmod"
else:
arg_name = "digest"

arg0 = call.get_argument(position=0, name="key")
if arg0.value in (
"secrets.token_bytes",
"secrets.token_hex",
"secrets.token_urlsafe",
):
symbol = context["symtab"].get(arg0.node.text.decode())
lastcall = symbol.call_history[-1]
nbytes = lastcall.get_argument(position=0, name="nbytes").value
key_size = nbytes if nbytes is not None else 32
elif arg0.is_str:
key_size = len(arg0.value_str)
else:
return

arg2 = call.get_argument(position=2, name=arg_name)
if arg2.value in HASHLIB_SIZES:
digest = arg2.value
min_digest_size = HASHLIB_SIZES.get(digest)
elif arg2.is_str and arg2.value_str in HASH_NAME_SIZES:
digest = arg2.value_str
min_digest_size = HASH_NAME_SIZES.get(digest)
else:
return

if key_size >= min_digest_size:
return

return Result(
rule_id=self.id,
location=Location(node=arg0.node),
message=self.message.format(key_size, min_digest_size, digest),
)
2 changes: 1 addition & 1 deletion precli/rules/python/stdlib/secrets_weak_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def analyze_call(self, context: dict, call: Call) -> Result:
return

arg = call.get_argument(position=0, name="nbytes")
nbytes = int(arg.value) if arg.node else 32
nbytes = int(arg.value) if arg.value else 32

if nbytes < 32:
fixes = Rule.get_fixes(
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,8 @@ precli.rules.python =
# precli/rules/python/stdlib/re_denial_of_service.py
PY033 = precli.rules.python.stdlib.re_denial_of_service:ReDenialOfService

# precli/rules/python/stdlib/hmac_weak_key.py
PY034 = precli.rules.python.stdlib.hmac_weak_key:HmacWeakKey

[build_sphinx]
all_files = 1
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# level: NONE
import hmac
import secrets


key = b"my-secret-key"
key = secrets.token_bytes(64)
message = b"Hello, world!"
hmac.digest(key, message, digest="blake2b")
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
import hmac


key = b"my-secret-key"
key = b"my-super-duper-secret-key-string"
message = b"Hello, world!"
hmac.digest(key, message, digest="blake2s")
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# level: NONE
import hashlib
import hmac
import secrets


key = b"my-secret-key"
key = secrets.token_bytes(64)
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.blake2b)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import hmac


key = b"my-secret-key"
key = b"my-super-duper-secret-key-string"
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.blake2s)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import hmac


key = b"my-secret-key"
key = b"my-super-duper-secret-key-string"
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.sha224)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import hmac


key = b"my-secret-key"
key = b"my-super-duper-secret-key-string"
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.sha256)
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# level: NONE
import hashlib
import hmac
from secrets import token_bytes


key = b"my-secret-key"
key = token_bytes(nbytes=48)
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.sha384)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import hmac


key = b"my-secret-key"
key = b"my-super-duper-secret-key-string"
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.sha3_224)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import hmac


key = b"my-secret-key"
key = b"my-super-duper-secret-key-string"
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.sha3_256)
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# level: NONE
import hashlib
import hmac
import secrets


key = b"my-secret-key"
key = secrets.token_bytes(nbytes=48)
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.sha3_384)
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# level: NONE
import hashlib
import hmac
import secrets


key = b"my-secret-key"
key = secrets.token_bytes(64)
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.sha3_512)
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# level: NONE
import hashlib
import hmac
import secrets


key = b"my-secret-key"
key = secrets.token_bytes(64)
message = b"Hello, world!"
hmac.digest(key, message, digest=hashlib.sha512)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
import hmac


key = b"my-secret-key"
key = b"my-super-duper-secret-key-string"
message = b"Hello, world!"
hmac.digest(key, message, digest="sha224")
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
import hmac


key = b"my-secret-key"
key = b"my-super-duper-secret-key-string"
message = b"Hello, world!"
hmac.digest(key, message, digest="sha256")
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# level: NONE
import hmac
from secrets import token_bytes


key = b"my-secret-key"
key = token_bytes(nbytes=48)
message = b"Hello, world!"
hmac.digest(key, message, digest="sha384")
Loading

0 comments on commit 48fe9de

Please sign in to comment.