pyca / cryptography

cryptography is a package designed to expose cryptographic primitives and recipes to Python developers.
https://cryptography.io
Other
6.67k stars 1.53k forks source link

KBKDFCMAC produces unexpected results when `llen=0` #11172

Closed DavidBuchanan314 closed 4 months ago

DavidBuchanan314 commented 4 months ago

Using cryptography version 41.0.7, installed via system package manager (python3-cryptography on Fedora 39).

I am trying to implement the XAES-256-GCM construction, as specified here: https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM.md (it combines the NIST SP 800-108r1 KDF, and AES-GCM)

As permitted by NIST SP 800-108r1, XAES-256-GCM specifies that the L field is omitted from the CMAC input. My first attempt at implementing this was to use KBKDFCMAC with llen=None. That gave me a ValueError: Please specify an llen, so then I tried llen=0, which returned a result, but not the result I was expecting:

from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.kdf.kbkdf import (
    CounterLocation, KBKDFCMAC, Mode
)

# first test vector input from https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM.md
K = bytes.fromhex("0101010101010101010101010101010101010101010101010101010101010101")
N = b"ABCDEFGHIJKLMNOPQRSTUVWX"

# Invocation 1:
# llen=0 (produces unexpected result)
kdf = KBKDFCMAC(
    algorithm=algorithms.AES256,
    mode=Mode.CounterMode,
    length=32,
    rlen=2,
    llen=0,
    location=CounterLocation.BeforeFixed,
    label=b"X",
    context=N[:12],
    fixed=None,
)
print(kdf.derive(K).hex()) # 0ac2f1727f8b128bb0b991c2ceaa600aea8128eab77a227f44c54ec235b330b0

# Invocation 2:
# llen=2 (produces same result as above)
kdf = KBKDFCMAC(
    algorithm=algorithms.AES256,
    mode=Mode.CounterMode,
    length=32,
    rlen=2,
    llen=2,
    location=CounterLocation.BeforeFixed,
    label=b"X",
    context=N[:12],
    fixed=None,
)
print(kdf.derive(K).hex()) # 0ac2f1727f8b128bb0b991c2ceaa600aea8128eab77a227f44c54ec235b330b0

# Invocation 3:
# fixed (produces expected result)
kdf = KBKDFCMAC(
    algorithm=algorithms.AES256,
    mode=Mode.CounterMode,
    length=32,
    rlen=2,
    llen=None,
    location=CounterLocation.BeforeFixed,
    label=None,
    context=None,
    fixed=b"X\x00" + N[:12],
)
print(kdf.derive(K).hex()) # c8612c9ed53fe43e8e005b828a1631a0bbcb6ab2f46514ec4f439fcfd0fa969b

I'm able to solve my problem by using the fixed parameter (as shown in "invocation 3" above), but I think the behaviour when llen=0 is unexpected/buggy.

It happens because utils.int_to_bytes is used to construct the L value, which has auto-sizing behaviour when the requested length is 0. For length=32, this produces equivalent results to passing llen=2 (as shown in "invocation 2" above)

https://github.com/pyca/cryptography/blob/986a6c22231bc5f587e9aab89d5a564b0aa80c63/src/cryptography/hazmat/primitives/kdf/kbkdf.py#L168-L174

IMHO, better behaviour would be to raise an exception when llen=0, rather than invoking this auto-sizing (I think switching from utils.int_to_bytes to regular int.to_bytes would achieve this). Technically this would be a breaking change, but I'd hope nobody is relying on it intentionally.

And, as an enhancement, it would be convenient if passing llen=None resulted in the L field being omitted entirely, as required by the XAES-256-GCM spec (and as permitted by NIST SP 800-108r1). This ought to be a non-breaking change, although the convenience it brings is relatively minor compared to just using the fixed parameter.

i.e.

kdf = KBKDFCMAC(
    algorithm=algorithms.AES256,
    mode=Mode.CounterMode,
    length=32,
    rlen=2,
    llen=None,
    location=CounterLocation.BeforeFixed,
    label=b"X",
    context=N[:12],
    fixed=None,
)
# current behaviour: ValueError: Please specify an llen
# desired behaviour: Equivalent to "invocation 3" above
alex commented 4 months ago

It looks to me like both int_to_bytes and KBKDF should be rejecting length=0. Would you be interested in sending a PR for that?

On Thu, Jun 27, 2024 at 7:48 AM David Buchanan @.***> wrote:

Using cryptography version 41.0.7, installed via system package manager ( python3-cryptography on Fedora 39).

I am trying to implement the XAES-256-GCM construction, as specified here: https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM.md (it combines the NIST SP 800-108r1 KDF, and AES-GCM)

As permitted by NIST SP 800-108r1, XAES-256-GCM specifies that the L field is omitted from the CMAC input. My first attempt at implementing this was to use KBKDFCMAC with llen=None. That gave me a ValueError: Please specify an llen, so then I tried llen=0, which returned a result, but not the result I was expecting:

from cryptography.hazmat.primitives.ciphers import algorithmsfrom cryptography.hazmat.primitives.kdf.kbkdf import ( CounterLocation, KBKDFCMAC, Mode )

first test vector input from https://github.com/C2SP/C2SP/blob/main/XAES-256-GCM.mdK = bytes.fromhex("0101010101010101010101010101010101010101010101010101010101010101")N = b"ABCDEFGHIJKLMNOPQRSTUVWX"

Invocation 1:# llen=0 (produces unexpected result)kdf = KBKDFCMAC(

algorithm=algorithms.AES256, mode=Mode.CounterMode, length=32, rlen=2, llen=0, location=CounterLocation.BeforeFixed, label=b"X", context=N[:12], fixed=None, )print(kdf.derive(K).hex()) # 0ac2f1727f8b128bb0b991c2ceaa600aea8128eab77a227f44c54ec235b330b0

Invocation 2:# llen=2 (produces same result as above)kdf = KBKDFCMAC(

algorithm=algorithms.AES256, mode=Mode.CounterMode, length=32, rlen=2, llen=2, location=CounterLocation.BeforeFixed, label=b"X", context=N[:12], fixed=None, )print(kdf.derive(K).hex()) # 0ac2f1727f8b128bb0b991c2ceaa600aea8128eab77a227f44c54ec235b330b0

Invocation 3:# fixed (produces expected result)kdf = KBKDFCMAC(

algorithm=algorithms.AES256, mode=Mode.CounterMode, length=32, rlen=2, llen=None, location=CounterLocation.BeforeFixed, label=None, context=None, fixed=b"X\x00" + N[:12], )print(kdf.derive(K).hex()) # c8612c9ed53fe43e8e005b828a1631a0bbcb6ab2f46514ec4f439fcfd0fa969b

I'm able to solve my problem by using the fixed parameter (as shown in "invocation 3" above), but I think the behaviour when llen=0 is unexpected/buggy.

It happens because utils.int_to_bytes is used to construct the L value, which has auto-sizing behaviour when the requested length is 0. For length=32, this produces equivalent results to passing llen=2 (as shown in "invocation 2" above)

https://github.com/pyca/cryptography/blob/986a6c22231bc5f587e9aab89d5a564b0aa80c63/src/cryptography/hazmat/primitives/kdf/kbkdf.py#L168-L174

IMHO, better behaviour would be to raise an exception when llen=0, rather than invoking this auto-sizing (I think switching from utils.int_to_bytes to regular int.to_bytes would achieve this). Technically this would be a breaking change, but I'd hope nobody is relying on it intentionally.

And, as an enhancement, it would be convenient if passing llen=None resulted in the L field being omitted entirely, as required by the XAES-256-GCM spec. This ought to be a non-breaking change, although the convenience it brings is relatively minor compared to just using the fixed parameter.

i.e.

kdf = KBKDFCMAC( algorithm=algorithms.AES256, mode=Mode.CounterMode, length=32, rlen=2, llen=None, location=CounterLocation.BeforeFixed, label=b"X", context=N[:12], fixed=None, )# current behaviour: ValueError: Please specify an llen# desired behaviour: Equivalent to "invocation 3" above

— Reply to this email directly, view it on GitHub https://github.com/pyca/cryptography/issues/11172, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAAGBAYGWANAEXCVAMX67TZJP32FAVCNFSM6AAAAABJ7YF5GCVHI2DSMVQWIX3LMV43ASLTON2WKOZSGM3TOOJTGI3DQNY . You are receiving this because you are subscribed to this thread.Message ID: @.***>

-- All that is necessary for evil to succeed is for good people to do nothing.

DavidBuchanan314 commented 4 months ago

Would you be interested in sending a PR for that?

Yup!

alex commented 4 months ago

Awesome!

On Thu, Jun 27, 2024 at 7:58 AM David Buchanan @.***> wrote:

Would you be interested in sending a PR for that?

Yup!

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: @.***>

-- All that is necessary for evil to succeed is for good people to do nothing.