AES keywrap support
diff --git a/docs/hazmat/primitives/index.rst b/docs/hazmat/primitives/index.rst
index a9ab38a..cf27622 100644
--- a/docs/hazmat/primitives/index.rst
+++ b/docs/hazmat/primitives/index.rst
@@ -11,6 +11,7 @@
     symmetric-encryption
     padding
     key-derivation-functions
+    keywrap
     asymmetric/index
     constant-time
     interfaces
diff --git a/docs/hazmat/primitives/keywrap.rst b/docs/hazmat/primitives/keywrap.rst
new file mode 100644
index 0000000..2ef6b79
--- /dev/null
+++ b/docs/hazmat/primitives/keywrap.rst
@@ -0,0 +1,43 @@
+.. hazmat::
+
+.. module:: cryptography.hazmat.primitives.keywrap
+
+Key wrapping
+============
+
+Key wrapping is a cryptographic construct that uses symmetric encryption to
+encapsulate key material.
+
+.. function:: aes_key_wrap(wrapping_key, key_to_wrap, backend)
+
+    :param bytes wrapping_key: The wrapping key.
+
+    :param bytes key_to_wrap: The key to wrap.
+
+    :param backend: A
+        :class:`~cryptography.hazmat.backends.interfaces.CipherBackend`
+        provider that supports
+        :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES`.
+
+    :return bytes: The wrapped key as bytes.
+
+.. function:: aes_key_unwrap(wrapping_key, wrapped_key, backend)
+
+    :param bytes wrapping_key: The wrapping key.
+
+    :param bytes wrapped_key: The wrapped key.
+
+    :param backend: A
+        :class:`~cryptography.hazmat.backends.interfaces.CipherBackend`
+        provider that supports
+        :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES`.
+
+    :return bytes: The unwrapped key as bytes.
+
+Exceptions
+~~~~~~~~~~
+
+.. class:: InvalidUnwrap
+
+    This is raised when a wrapped key fails to unwrap. It can be caused by a
+    corrupted or invalid wrapped key or an invalid wrapping key.
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index 58587b9..4c3402f 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -41,7 +41,7 @@
     _DISTPOINT_TYPE_FULLNAME, _DISTPOINT_TYPE_RELATIVENAME
 )
 from cryptography.hazmat.bindings.openssl import binding
-from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives import hashes, keywrap, serialization
 from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
 from cryptography.hazmat.primitives.asymmetric.padding import (
     MGF1, OAEP, PKCS1v15, PSS
diff --git a/src/cryptography/hazmat/primitives/keywrap.py b/src/cryptography/hazmat/primitives/keywrap.py
new file mode 100644
index 0000000..89925f3
--- /dev/null
+++ b/src/cryptography/hazmat/primitives/keywrap.py
@@ -0,0 +1,84 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+import struct
+
+from cryptography.hazmat.primitives.ciphers import Cipher
+from cryptography.hazmat.primitives.ciphers.algorithms import AES
+from cryptography.hazmat.primitives.ciphers.modes import ECB
+from cryptography.hazmat.primitives.constant_time import bytes_eq
+
+
+def aes_key_wrap(wrapping_key, key_to_wrap, backend):
+    if len(wrapping_key) not in [16, 24, 32]:
+        raise ValueError("The wrapping key must be a valid AES key length")
+
+    if len(key_to_wrap) < 16:
+        raise ValueError("The key to wrap must be at least 16 bytes")
+
+    if len(key_to_wrap) % 8 != 0:
+        raise ValueError("The key to wrap must be a multiple of 8 bytes")
+
+    # RFC 3394 Key Wrap - 2.2.1 (index method)
+    encryptor = Cipher(AES(wrapping_key), ECB(), backend).encryptor()
+    a = b"\xa6\xa6\xa6\xa6\xa6\xa6\xa6\xa6"
+    r = [key_to_wrap[i:i + 8] for i in range(0, len(key_to_wrap), 8)]
+    n = len(r)
+    for j in range(6):
+        for i in range(n):
+            # every encryption operation is a discrete 16 byte chunk so
+            # it is safe to reuse the encryptor for the entire operation
+            b = encryptor.update(a + r[i])
+            # pack/unpack are safe as these are always 64-bit chunks
+            a = struct.pack(
+                ">Q", struct.unpack(">Q", b[:8])[0] ^ ((n * j) + i + 1)
+            )
+            r[i] = b[-8:]
+
+    assert encryptor.finalize() == b""
+
+    return a + b"".join(r)
+
+
+def aes_key_unwrap(wrapping_key, wrapped_key, backend):
+    if len(wrapped_key) < 24:
+        raise ValueError("Must be at least 24 bytes")
+
+    if len(wrapped_key) % 8 != 0:
+        raise ValueError("The wrapped key must be a multiple of 8 bytes")
+
+    if len(wrapping_key) not in [16, 24, 32]:
+        raise ValueError("The wrapping key must be a valid AES key length")
+
+    # Implement RFC 3394 Key Unwrap - 2.2.2 (index method)
+    decryptor = Cipher(AES(wrapping_key), ECB(), backend).decryptor()
+    aiv = b"\xa6\xa6\xa6\xa6\xa6\xa6\xa6\xa6"
+
+    r = [wrapped_key[i:i + 8] for i in range(0, len(wrapped_key), 8)]
+    a = r.pop(0)
+    n = len(r)
+    for j in reversed(range(6)):
+        for i in reversed(range(n)):
+            # pack/unpack are safe as these are always 64-bit chunks
+            atr = struct.pack(
+                ">Q", struct.unpack(">Q", a)[0] ^ ((n * j) + i + 1)
+            ) + r[i]
+            # every decryption operation is a discrete 16 byte chunk so
+            # it is safe to reuse the decryptor for the entire operation
+            b = decryptor.update(atr)
+            a = b[:8]
+            r[i] = b[-8:]
+
+    assert decryptor.finalize() == b""
+
+    if not bytes_eq(a, aiv):
+        raise InvalidUnwrap()
+
+    return b"".join(r)
+
+
+class InvalidUnwrap(Exception):
+    pass
diff --git a/tests/hazmat/primitives/test_keywrap.py b/tests/hazmat/primitives/test_keywrap.py
new file mode 100644
index 0000000..f49cdad
--- /dev/null
+++ b/tests/hazmat/primitives/test_keywrap.py
@@ -0,0 +1,112 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+import binascii
+import os
+
+import pytest
+
+from cryptography.hazmat.backends.interfaces import CipherBackend
+from cryptography.hazmat.primitives import keywrap
+from cryptography.hazmat.primitives.ciphers import algorithms, modes
+
+from .utils import _load_all_params
+from ...utils import load_nist_vectors
+
+
+@pytest.mark.requires_backend_interface(interface=CipherBackend)
+class TestAESKeyWrap(object):
+    @pytest.mark.parametrize(
+        "params",
+        _load_all_params(
+            os.path.join("keywrap", "kwtestvectors"),
+            ["KW_AE_128.txt", "KW_AE_192.txt", "KW_AE_256.txt"],
+            load_nist_vectors
+        )
+    )
+    @pytest.mark.supported(
+        only_if=lambda backend: backend.cipher_supported(
+            algorithms.AES("\x00" * 16), modes.ECB()
+        ),
+        skip_message="Does not support AES key wrap (RFC 3394)",
+    )
+    def test_wrap(self, backend, params):
+        wrapping_key = binascii.unhexlify(params["k"])
+        key_to_wrap = binascii.unhexlify(params["p"])
+        wrapped_key = keywrap.aes_key_wrap(wrapping_key, key_to_wrap, backend)
+        assert params["c"] == binascii.hexlify(wrapped_key)
+
+    @pytest.mark.parametrize(
+        "params",
+        _load_all_params(
+            os.path.join("keywrap", "kwtestvectors"),
+            ["KW_AD_128.txt", "KW_AD_192.txt", "KW_AD_256.txt"],
+            load_nist_vectors
+        )
+    )
+    @pytest.mark.supported(
+        only_if=lambda backend: backend.cipher_supported(
+            algorithms.AES("\x00" * 16), modes.ECB()
+        ),
+        skip_message="Does not support AES key wrap (RFC 3394)",
+    )
+    def test_unwrap(self, backend, params):
+        wrapping_key = binascii.unhexlify(params["k"])
+        wrapped_key = binascii.unhexlify(params["c"])
+        if params.get("fail") is True:
+            with pytest.raises(keywrap.InvalidUnwrap):
+                keywrap.aes_key_unwrap(wrapping_key, wrapped_key, backend)
+        else:
+            unwrapped_key = keywrap.aes_key_unwrap(
+                wrapping_key, wrapped_key, backend
+            )
+            assert params["p"] == binascii.hexlify(unwrapped_key)
+
+    @pytest.mark.supported(
+        only_if=lambda backend: backend.cipher_supported(
+            algorithms.AES("\x00" * 16), modes.ECB()
+        ),
+        skip_message="Does not support AES key wrap (RFC 3394)",
+    )
+    def test_wrap_invalid_key_length(self, backend):
+        with pytest.raises(ValueError):
+            keywrap.aes_key_wrap(b"badkey", b"sixteen_byte_key", backend)
+
+    @pytest.mark.supported(
+        only_if=lambda backend: backend.cipher_supported(
+            algorithms.AES("\x00" * 16), modes.ECB()
+        ),
+        skip_message="Does not support AES key wrap (RFC 3394)",
+    )
+    def test_unwrap_invalid_key_length(self, backend):
+        with pytest.raises(ValueError):
+            keywrap.aes_key_unwrap(b"badkey", b"\x00" * 24, backend)
+
+    @pytest.mark.supported(
+        only_if=lambda backend: backend.cipher_supported(
+            algorithms.AES("\x00" * 16), modes.ECB()
+        ),
+        skip_message="Does not support AES key wrap (RFC 3394)",
+    )
+    def test_wrap_invalid_key_to_wrap_length(self, backend):
+        with pytest.raises(ValueError):
+            keywrap.aes_key_wrap(b"sixteen_byte_key", b"\x00" * 15, backend)
+
+        with pytest.raises(ValueError):
+            keywrap.aes_key_wrap(b"sixteen_byte_key", b"\x00" * 23, backend)
+
+    @pytest.mark.supported(
+        only_if=lambda backend: backend.cipher_supported(
+            algorithms.AES("\x00" * 16), modes.ECB()
+        ),
+        skip_message="Does not support AES key wrap (RFC 3394)",
+    )
+    def test_unwrap_invalid_wrapped_key_length(self, backend):
+        with pytest.raises(ValueError):
+            keywrap.aes_key_unwrap(b"sixteen_byte_key", b"\x00" * 16, backend)
+
+        with pytest.raises(ValueError):
+            keywrap.aes_key_unwrap(b"sixteen_byte_key", b"\x00" * 27, backend)