#!/usr/bin/env python # -*- coding: utf-8 -*- #======================================================================= # # keywrap.py # ---------- # Python model to test AES KEY WRAP according to RFC 5649. # # # Author: Joachim Strombergson # Copyright (c) 2018, NORDUnet A/S # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # - Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # - Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # - Neither the name of the NORDUnet nor the names of its contributors may # be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # #======================================================================= #------------------------------------------------------------------- # Python module imports. #------------------------------------------------------------------- import sys import Crypto.Random from Crypto.Cipher import AES from struct import pack, unpack #------------------------------------------------------------------- # Constants. #------------------------------------------------------------------- VERBOSE = True #------------------------------------------------------------------- # AESKeyWrapWithPadding #------------------------------------------------------------------- class AESKeyWrapWithPadding(object): """ Implementation of AES Key Wrap With Padding from RFC 5649. """ class UnwrapError(Exception): "Something went wrong during unwrap." def __init__(self, key): self.ctx = AES.new(key, AES.MODE_ECB) def _encrypt(self, b1, b2): aes_block = self.ctx.encrypt(b1 + b2) return aes_block[:8], aes_block[8:] def _decrypt(self, b1, b2): aes_block = self.ctx.decrypt(b1 + b2) return aes_block[:8], aes_block[8:] @staticmethod def _start_stop(start, stop): # Syntactic sugar step = -1 if start > stop else 1 return xrange(start, stop + step, step) @staticmethod def _xor(R0, t): return pack(">Q", unpack(">Q", R0)[0] ^ t) def wrap(self, Q): "RFC 5649 section 4.1." m = len(Q) # Plaintext length if m % 8 != 0: # Pad Q if needed Q += "\x00" * (8 - (m % 8)) R = [pack(">LL", 0xa65959a6, m)] # Magic MSB(32,A), build LSB(32,A) R.extend(Q[i : i + 8] # Append Q for i in xrange(0, len(Q), 8)) n = len(R) - 1 if n == 1: R[0], R[1] = self._encrypt(R[0], R[1]) else: # RFC 3394 section 2.2.1 for j in self._start_stop(0, 5): for i in self._start_stop(1, n): R[0], R[i] = self._encrypt(R[0], R[i]) R[0] = self._xor(R[0], n * j + i) assert len(R) == (n + 1) and all(len(r) == 8 for r in R) return "".join(R) def unwrap(self, C): "RFC 5649 section 4.2." if len(C) % 8 != 0: raise self.UnwrapError("Ciphertext length {} is not an integral number of blocks" .format(len(C))) n = (len(C) / 8) - 1 R = [C[i : i + 8] for i in xrange(0, len(C), 8)] if n == 1: R[0], R[1] = self._decrypt(R[0], R[1]) else: # RFC 3394 section 2.2.2 steps (1), (2), and part of (3) for j in self._start_stop(5, 0): for i in self._start_stop(n, 1): R[0] = self._xor(R[0], n * j + i) R[0], R[i] = self._decrypt(R[0], R[i]) magic, m = unpack(">LL", R[0]) if magic != 0xa65959a6: raise self.UnwrapError("Magic value in AIV should have been 0xa65959a6, was 0x{:02x}" .format(magic)) if m <= 8 * (n - 1) or m > 8 * n: raise self.UnwrapError("Length encoded in AIV out of range: m {}, n {}".format(m, n)) R = "".join(R[1:]) assert len(R) == 8 * n if any(r != "\x00" for r in R[m:]): raise self.UnwrapError("Nonzero trailing bytes {}".format(R[m:].encode("hex"))) return R[:m] #------------------------------------------------------------------- # wrap_test1 # # First, simplest test from NIST test vectors. #------------------------------------------------------------------- def wrap_test1(): my_key = Crypto.Random.new().read(256/8) my_keywrap = AESKeyWrapWithPadding(my_key) my_plaintext = "\x31\x32\x33" my_wrap = my_keywrap.wrap(my_plaintext) print(type(my_wrap)) my_unwrap = my_keywrap.wrap(my_wrap) print(type(my_unwrap)) print("plaintext: %s wrapped: %s unwrapped: %s" % (my_plaintext, my_wrap, my_unwrap)) #------------------------------------------------------------------- #------------------------------------------------------------------- def main(): print("Testing the Key Wrap Python model") print("=================================") print # keywrap_aes_test() wrap_test1() #------------------------------------------------------------------- # __name__ # Python thingy which allows the file to be run standalone as # well as parsed from within a Python interpreter. #------------------------------------------------------------------- if __name__=="__main__": # Run the main function. sys.exit(main()) #======================================================================= # EOF keywrap.py #=======================================================================