Skip to content

Commit cd6cf4a

Browse files
reaperhulkalex
authored andcommitted
implement AES KW with padding (RFC 5649) (pyca#3880)
* implement AES KW with padding (RFC 5649) fixes pyca#3791 * oops, 2.2 * make sure this is the right valueerror * more match * make key padding easier to read * review feedback * review feedback
1 parent 4a41e54 commit cd6cf4a

4 files changed

Lines changed: 166 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ Changelog
1818
:meth:`~cryptography.fernet.MultiFernet.rotate`.
1919
* Fixed a memory leak in
2020
:func:`~cryptography.hazmat.primitives.asymmetric.ec.derive_private_key`.
21+
* Added support for AES key wrapping with padding via
22+
:func:`~cryptography.hazmat.primitives.keywrap.aes_key_wrap_with_padding`
23+
and
24+
:func:`~cryptography.hazmat.primitives.keywrap.aes_key_unwrap_with_padding`
25+
.
2126

2227
.. _v2-1-4:
2328

docs/hazmat/primitives/keywrap.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,45 @@ protections offered by key wrapping are also offered by using authenticated
5050
:raises cryptography.hazmat.primitives.keywrap.InvalidUnwrap: This is
5151
raised if the key is not successfully unwrapped.
5252

53+
.. function:: aes_key_wrap_with_padding(wrapping_key, key_to_wrap, backend)
54+
55+
.. versionadded:: 2.2
56+
57+
This function performs AES key wrap with padding as specified in
58+
:rfc:`5649`.
59+
60+
:param bytes wrapping_key: The wrapping key.
61+
62+
:param bytes key_to_wrap: The key to wrap.
63+
64+
:param backend: A
65+
:class:`~cryptography.hazmat.backends.interfaces.CipherBackend`
66+
instance that supports
67+
:class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES`.
68+
69+
:return bytes: The wrapped key as bytes.
70+
71+
.. function:: aes_key_unwrap_with_padding(wrapping_key, wrapped_key, backend)
72+
73+
.. versionadded:: 2.2
74+
75+
This function performs AES key unwrap with padding as specified in
76+
:rfc:`5649`.
77+
78+
:param bytes wrapping_key: The wrapping key.
79+
80+
:param bytes wrapped_key: The wrapped key.
81+
82+
:param backend: A
83+
:class:`~cryptography.hazmat.backends.interfaces.CipherBackend`
84+
instance that supports
85+
:class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES`.
86+
87+
:return bytes: The unwrapped key as bytes.
88+
89+
:raises cryptography.hazmat.primitives.keywrap.InvalidUnwrap: This is
90+
raised if the key is not successfully unwrapped.
91+
5392
Exceptions
5493
~~~~~~~~~~
5594

src/cryptography/hazmat/primitives/keywrap.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,63 @@ def _unwrap_core(wrapping_key, a, r, backend):
6868
return a, r
6969

7070

71+
def aes_key_wrap_with_padding(wrapping_key, key_to_wrap, backend):
72+
if len(wrapping_key) not in [16, 24, 32]:
73+
raise ValueError("The wrapping key must be a valid AES key length")
74+
75+
aiv = b"\xA6\x59\x59\xA6" + struct.pack(">i", len(key_to_wrap))
76+
# pad the key to wrap if necessary
77+
pad = (8 - (len(key_to_wrap) % 8)) % 8
78+
key_to_wrap = key_to_wrap + b"\x00" * pad
79+
if len(key_to_wrap) == 8:
80+
# RFC 5649 - 4.1 - exactly 8 octets after padding
81+
encryptor = Cipher(AES(wrapping_key), ECB(), backend).encryptor()
82+
b = encryptor.update(aiv + key_to_wrap)
83+
assert encryptor.finalize() == b""
84+
return b
85+
else:
86+
r = [key_to_wrap[i:i + 8] for i in range(0, len(key_to_wrap), 8)]
87+
return _wrap_core(wrapping_key, aiv, r, backend)
88+
89+
90+
def aes_key_unwrap_with_padding(wrapping_key, wrapped_key, backend):
91+
if len(wrapped_key) < 16:
92+
raise ValueError("Must be at least 16 bytes")
93+
94+
if len(wrapping_key) not in [16, 24, 32]:
95+
raise ValueError("The wrapping key must be a valid AES key length")
96+
97+
if len(wrapped_key) == 16:
98+
# RFC 5649 - 4.2 - exactly two 64-bit blocks
99+
decryptor = Cipher(AES(wrapping_key), ECB(), backend).decryptor()
100+
b = decryptor.update(wrapped_key)
101+
assert decryptor.finalize() == b""
102+
a = b[:8]
103+
data = b[8:]
104+
n = 1
105+
else:
106+
r = [wrapped_key[i:i + 8] for i in range(0, len(wrapped_key), 8)]
107+
encrypted_aiv = r.pop(0)
108+
n = len(r)
109+
a, r = _unwrap_core(wrapping_key, encrypted_aiv, r, backend)
110+
data = b"".join(r)
111+
112+
# 1) Check that MSB(32,A) = A65959A6.
113+
# 2) Check that 8*(n-1) < LSB(32,A) <= 8*n. If so, let
114+
# MLI = LSB(32,A).
115+
# 3) Let b = (8*n)-MLI, and then check that the rightmost b octets of
116+
# the output data are zero.
117+
(mli,) = struct.unpack(">I", a[4:])
118+
b = (8 * n) - mli
119+
if (
120+
not bytes_eq(a[:4], b"\xa6\x59\x59\xa6") or not
121+
8 * (n - 1) < mli <= 8 * n or not bytes_eq(data[-b:], b"\x00" * b)
122+
):
123+
raise InvalidUnwrap()
124+
125+
return data[:-b]
126+
127+
71128
def aes_key_unwrap(wrapping_key, wrapped_key, backend):
72129
if len(wrapped_key) < 24:
73130
raise ValueError("Must be at least 24 bytes")

tests/hazmat/primitives/test_keywrap.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,68 @@ def test_unwrap_invalid_wrapped_key_length(self, backend):
114114
# Keys to unwrap must be a multiple of 8 bytes
115115
with pytest.raises(ValueError):
116116
keywrap.aes_key_unwrap(b"sixteen_byte_key", b"\x00" * 27, backend)
117+
118+
119+
@pytest.mark.supported(
120+
only_if=lambda backend: backend.cipher_supported(
121+
algorithms.AES(b"\x00" * 16), modes.ECB()
122+
),
123+
skip_message="Does not support AES key wrap (RFC 5649) because AES-ECB"
124+
" is unsupported",
125+
)
126+
@pytest.mark.requires_backend_interface(interface=CipherBackend)
127+
class TestAESKeyWrapWithPadding(object):
128+
@pytest.mark.parametrize(
129+
"params",
130+
_load_all_params(
131+
os.path.join("keywrap", "kwtestvectors"),
132+
["KWP_AE_128.txt", "KWP_AE_192.txt", "KWP_AE_256.txt"],
133+
load_nist_vectors
134+
)
135+
)
136+
def test_wrap(self, backend, params):
137+
wrapping_key = binascii.unhexlify(params["k"])
138+
key_to_wrap = binascii.unhexlify(params["p"])
139+
wrapped_key = keywrap.aes_key_wrap_with_padding(
140+
wrapping_key, key_to_wrap, backend
141+
)
142+
assert params["c"] == binascii.hexlify(wrapped_key)
143+
144+
@pytest.mark.parametrize(
145+
"params",
146+
_load_all_params(
147+
os.path.join("keywrap", "kwtestvectors"),
148+
["KWP_AD_128.txt", "KWP_AD_192.txt", "KWP_AD_256.txt"],
149+
load_nist_vectors
150+
)
151+
)
152+
def test_unwrap(self, backend, params):
153+
wrapping_key = binascii.unhexlify(params["k"])
154+
wrapped_key = binascii.unhexlify(params["c"])
155+
if params.get("fail") is True:
156+
with pytest.raises(keywrap.InvalidUnwrap):
157+
keywrap.aes_key_unwrap_with_padding(
158+
wrapping_key, wrapped_key, backend
159+
)
160+
else:
161+
unwrapped_key = keywrap.aes_key_unwrap_with_padding(
162+
wrapping_key, wrapped_key, backend
163+
)
164+
assert params["p"] == binascii.hexlify(unwrapped_key)
165+
166+
def test_unwrap_invalid_wrapped_key_length(self, backend):
167+
# Keys to unwrap must be at least 16 bytes
168+
with pytest.raises(ValueError, match='Must be at least 16 bytes'):
169+
keywrap.aes_key_unwrap_with_padding(
170+
b"sixteen_byte_key", b"\x00" * 15, backend
171+
)
172+
173+
def test_wrap_invalid_key_length(self, backend):
174+
with pytest.raises(ValueError, match='must be a valid AES key length'):
175+
keywrap.aes_key_wrap_with_padding(b"badkey", b"\x00", backend)
176+
177+
def test_unwrap_invalid_key_length(self, backend):
178+
with pytest.raises(ValueError, match='must be a valid AES key length'):
179+
keywrap.aes_key_unwrap_with_padding(
180+
b"badkey", b"\x00" * 16, backend
181+
)

0 commit comments

Comments
 (0)