Skip to content
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

Rule to check for insufficient key size to HMAC #390

Merged
merged 1 commit into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
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")
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="sha3_224")
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()
message = b"Hello, world!"
hmac.digest(key, message, digest="sha3_256")
Loading