aboutsummaryrefslogblamecommitdiff
path: root/utils/Makefile
blob: ce36d4c8a9fc5ec743201a41d68000070922cdd1 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                       
                      
 




                                                                          
 


                                                                        
 


                                                                          
 










                                                                          
 


                                      
 

                                                        
 









                                                                                                            
 


                        
                      

                                           
                          
                                   
# Copyright (c) 2015-2016, 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.

ifndef CRYPTECH_ROOT
  CRYPTECH_ROOT := $(abspath ../../..)
endif

LIBTFM_SRC	?= ${CRYPTECH_ROOT}/sw/thirdparty/libtfm
LIBTFM_BLD	?= ${LIBTFM_SRC}

LIBHAL_SRC	?= ${CRYPTECH_ROOT}/sw/libhal
LIBHAL_BLD	?= ${LIBHAL_SRC}

LIBS		= ${LIBHAL_BLD}/libhal.a ${LIBTFM_BLD}/libtfm.a

CFLAGS		?= -g3 -Wall -fPIC -std=c99 -I${LIBHAL_SRC} -I${LIBTFM_BLD}

BIN		= eim_peek_poke cores

all: $(if $(wildcard ${LIBHAL_BLD}/hal_io_eim.o),eim_peek_poke) $(if $(wildcard ${LIBHAL_BLD}/core.o),cores)

clean:
	rm -f *.o ${BIN}

${BIN}: %: %.o ${LIBS}
	${CC} ${CFLAGS} -o $@ $^ ${LDFLAGS}

%.o: %.c ${LIBHAL_SRC}/*.h
	${CC} ${CFLAGS} -c -o $@ $<
3 244 245 246 247 248 249 250 251 252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446

                     
















                                                                      








                                                                    





                                                          




                                                                             


                                                              
 
                                                                           








                                                                     
                             



                                     
                                                               



                                                                                                 





                                                                   

                        








                                                
                       

                                                  




                                                       































                                                                 










































                                                                                  
               
 


                                        



                                             
                                                                            


                                                                             
                                          

















                                                                                          




                                                         
                                                                
                           





























                                                                                 



















                                                                                        






                                                              
                           










                                                                       







                                                                                  











                                                                                        























































































































































                                                                                                 


                          
#!/usr/bin/env python

"""
Securely back up private keys from one Cryptech HSM to another.

This works by having the destination HSM (the one importing keys)
create an RSA keypair (the "KEKEK"), the public key of which can then
be imported into the source HSM (the one exporting keys) and used to
encrypt AES key encryption keys (KEKs) which in turn can be used to
wrap the private keys being transfered.  Transfers are encoded in
JSON; the underlying ASN.1 formats are SubjectPublicKeyInfo (KEKEK
public key) and PKCS #8 EncryptedPrivateKeyInfo (everything else).

NOTE WELL: while this process makes it POSSIBLE to back up keys
securely, it is not sufficient by itself: the operator MUST make
sure only to export keys using a KEKEK known to have been generated by
the target HSM.  See the unit tests in the source repository for
an example of how to fake this in a few lines of Python.

We also implement a software-based variant on this backup mechanism,
for cases where there is no second HSM.  The protocol is much the
same, but the KEKEK is generated in software and encrypted using a
symmetric key derived from a passphrase using PBKDF2.  This requires
the PyCrypto library, and is only as secure as memory on the machine
where you're running it (so it's theoretically vulnerable to root or
anybody with access to /dev/mem).  Don't use this mode unless you
understand the risks, and see the "NOTE WELL" above.

YOU HAVE BEEN WARNED.  Be careful out there.
"""

# Diagram of the trivial protocol we're using:
#
#    SOURCE HSM                            DESTINATION HSM
#
#                                          Generate and export KEKEK:
#                                               hal_rpc_pkey_generate_rsa()
#                                               hal_rpc_pkey_get_public_key()
#
#   Load KEKEK public        <---------    Export KEKEK public
#        hal_rpc_pkey_load()
#        hal_rpc_pkey_export()
#
#   Export PKCS #8 and KEK   ---------->   Load PKCS #8 and KEK, import key
#                                               hal_rpc_pkey_import()

import sys
import json
import uuid
import atexit
import getpass
import argparse

from cryptech.libhal import *

def main():

    parser = argparse.ArgumentParser(
        formatter_class = argparse.RawDescriptionHelpFormatter,
        description = __doc__)
    subparsers = parser.add_subparsers(
        title = "Commands (use \"--help\" after command name for help with individual commands)",
        metavar = "")
    setup_parser  = defcmd(subparsers, cmd_setup)
    export_parser = defcmd(subparsers, cmd_export)
    import_parser = defcmd(subparsers, cmd_import)
    setup_mutex_group = setup_parser.add_mutually_exclusive_group()


    parser.add_argument(
        "-p", "--pin",
        help    = "wheel PIN")


    setup_mutex_group.add_argument(
        "-n", "--new",
        action  = "store_true",
        help    = "force creation of new KEKEK")

    setup_mutex_group.add_argument(
        "-u", "--uuid",
        help    = "UUID of existing KEKEK to use")

    setup_mutex_group.add_argument(
        "-s", "--soft-backup",
        action = "store_true",
        help   = "software-based backup, see warnings")

    setup_parser.add_argument(
        "-k", "--keylen",
        type    = int,
        default = 2048,
        help    = "length of new KEKEK if we need to create one")

    setup_parser.add_argument(
        "-o", "--output",
        type    = argparse.FileType("w"),
        default = "-",
        help    = "output file")


    export_parser.add_argument(
        "-i", "--input",
        type    = argparse.FileType("r"),
        default = "-",
        help    = "input file")

    export_parser.add_argument(
        "-o", "--output",
        type    = argparse.FileType("w"),
        default = "-",
        help    = "output file")


    import_parser.add_argument(
        "-i", "--input",
        type    = argparse.FileType("r"),
        default = "-",
        help    = "input file")


    args = parser.parse_args()

    hsm = HSM()

    try:
        hsm.login(HAL_USER_WHEEL, args.pin or getpass.getpass("Wheel PIN: "))

    except HALError as e:
        sys.exit("Couldn't log into HSM: {}".format(e))

    try:
        sys.exit(args.func(args, hsm))

    finally:
        hsm.logout()


def defcmd(subparsers, func):
    assert func.__name__.startswith("cmd_")
    subparser = subparsers.add_parser(func.__name__[4:],
                                      description = func.__doc__,
                                      help = func.__doc__.strip().splitlines()[0])
    subparser.set_defaults(func = func)
    return subparser


def b64(bytes):
    return bytes.encode("base64").splitlines()

def b64join(lines):
    return "".join(lines).decode("base64")


def cmd_setup(args, hsm):
    """
    Set up backup HSM for subsequent import.
    Generates an RSA keypair with appropriate usage settings
    to use as a key-encryption-key-encryption-key (KEKEK), and
    writes the KEKEK to a JSON file for transfer to primary HSM.
    """

    result = {}
    uuids  = []

    if args.soft_backup:
        SoftKEKEK.generate(args, result)
    elif args.uuid:
        uuids.append(args.uuid)
    elif not args.new:
        uuids.extend(hsm.pkey_match(
            type  = HAL_KEY_TYPE_RSA_PRIVATE,
            mask  = HAL_KEY_FLAG_USAGE_KEYENCIPHERMENT | HAL_KEY_FLAG_TOKEN,
            flags = HAL_KEY_FLAG_USAGE_KEYENCIPHERMENT | HAL_KEY_FLAG_TOKEN))

    for uuid in uuids:
        with hsm.pkey_open(uuid) as kekek:
            if kekek.key_type != HAL_KEY_TYPE_RSA_PRIVATE:
                sys.stderr.write("Key {} is not an RSA private key\n".format(uuid))
            elif (kekek.key_flags & HAL_KEY_FLAG_USAGE_KEYENCIPHERMENT) == 0:
                sys.stderr.write("Key {} does not allow key encipherment\n".format(uuid))
            else:
                result.update(kekek_uuid   = str(kekek.uuid),
                              kekek_pubkey = b64(kekek.public_key))
                break

    if not result and not args.uuid:
        with hsm.pkey_generate_rsa(
                keylen = args.keylen,
                flags = HAL_KEY_FLAG_USAGE_KEYENCIPHERMENT | HAL_KEY_FLAG_TOKEN) as kekek:
            result.update(kekek_uuid   = str(kekek.uuid),
                          kekek_pubkey = b64(kekek.public_key))
    if not result:
        sys.exit("Could not find suitable KEKEK")

    if args.soft_backup:
        result.update(comment = "KEKEK software keypair")
    else:
        result.update(comment = "KEKEK public key")

    json.dump(result, args.output, indent = 4, sort_keys = True)
    args.output.write("\n")


def key_flag_names(flags):
    names = dict(digitalsignature = HAL_KEY_FLAG_USAGE_DIGITALSIGNATURE,
                 keyencipherment  = HAL_KEY_FLAG_USAGE_KEYENCIPHERMENT,
                 dataencipherment = HAL_KEY_FLAG_USAGE_DATAENCIPHERMENT,
                 token            = HAL_KEY_FLAG_TOKEN,
                 public           = HAL_KEY_FLAG_PUBLIC,
                 exportable       = HAL_KEY_FLAG_EXPORTABLE)
    return ", ".join(sorted(k for k, v in names.iteritems() if (flags & v) != 0))


def cmd_export(args, hsm):
    """
    Export encrypted keys from primary HSM.
    Takes a JSON file containing KEKEK (generated by running this
    script's "setup" command against the backup HSM), installs that
    key on the primary HSM, and backs up keys encrypted to the KEKEK
    by writing them to another JSON file for transfer to the backup HSM.
    """

    db = json.load(args.input)

    result = []

    kekek = None
    try:
        kekek = hsm.pkey_load(der   = b64join(db["kekek_pubkey"]),
                              flags = HAL_KEY_FLAG_USAGE_KEYENCIPHERMENT)

        for uuid in hsm.pkey_match(mask  = HAL_KEY_FLAG_EXPORTABLE,
                                   flags = HAL_KEY_FLAG_EXPORTABLE):
            with hsm.pkey_open(uuid) as pkey:

                if pkey.key_type in (HAL_KEY_TYPE_RSA_PRIVATE, HAL_KEY_TYPE_EC_PRIVATE):
                    pkcs8, kek = kekek.export_pkey(pkey)
                    result.append(dict(
                        comment = "Encrypted private key",
                        pkcs8   = b64(pkcs8),
                        kek     = b64(kek),
                        uuid    = str(pkey.uuid),
                        flags   = pkey.key_flags))

                elif pkey.key_type in (HAL_KEY_TYPE_RSA_PUBLIC, HAL_KEY_TYPE_EC_PUBLIC):
                    result.append(dict(
                        comment = "Public key",
                        spki    = b64(pkey.public_key),
                        uuid    = str(pkey.uuid),
                        flags   = pkey.key_flags))

    finally:
        if kekek is not None:
            kekek.delete()

    db.update(comment = "Cryptech Alpha encrypted key backup",
              keys    = result)
    json.dump(db, args.output, indent = 4, sort_keys = True)
    args.output.write("\n")


def cmd_import(args, hsm):
    """
    Import encrypted keys into backup HSM.
    Takes a JSON file containing a key backup (generated by running
    this script's "export" command against the primary HSM) and imports
    keys into the backup HSM.
    """

    db = json.load(args.input)

    soft_key = SoftKEKEK.is_soft_key(db)

    with (hsm.pkey_load(SoftKEKEK.recover(db), HAL_KEY_FLAG_USAGE_KEYENCIPHERMENT)
          if soft_key else
          hsm.pkey_open(uuid.UUID(db["kekek_uuid"]).bytes)
    ) as kekek:

        for k in db["keys"]:
            pkcs8 = b64join(k.get("pkcs8", ""))
            spki  = b64join(k.get("spki",  ""))
            kek   = b64join(k.get("kek",   ""))
            flags =         k.get("flags",  0)
            if pkcs8 and kek:
                with kekek.import_pkey(pkcs8 = pkcs8, kek = kek, flags = flags) as pkey:
                    print "Imported {} as {}".format(k["uuid"], pkey.uuid)
            elif spki:
                with hsm.pkey_load(der = spki, flags = flags) as pkey:
                    print "Loaded {} as {}".format(k["uuid"], pkey.uuid)

        if soft_key:
            kekek.delete()


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):
        from Crypto.Cipher import AES
        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):
        from struct import pack, unpack
        return pack(">Q", unpack(">Q", R0)[0] ^ t)

    def wrap(self, Q):
        "RFC 5649 section 4.1."
        from struct import pack
        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."
        from struct import unpack
        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]


class SoftKEKEK(object):
    """
    Wrapper around all the goo we need to implement soft backups.
    Requires PyCrypto on about every other line.
    """

    oid_aesKeyWrap = "\x60\x86\x48\x01\x65\x03\x04\x01\x30"

    def parse_EncryptedPrivateKeyInfo(self, der):
        from Crypto.Util.asn1 import DerObject, DerSequence, DerOctetString, DerObjectId
        encryptedPrivateKeyInfo = DerSequence()
        encryptedPrivateKeyInfo.decode(der)
        encryptionAlgorithm = DerSequence()
        algorithm = DerObjectId()
        encryptedData = DerOctetString()
        encryptionAlgorithm.decode(encryptedPrivateKeyInfo[0])
        DerObject.decode(algorithm, encryptionAlgorithm[0])
        DerObject.decode(encryptedData, encryptedPrivateKeyInfo[1])
        if algorithm.payload != self.oid_aesKeyWrap:
            raise ValueError
        return encryptedData.payload

    def encode_EncryptedPrivateKeyInfo(self, der):
        from Crypto.Util.asn1 import DerSequence, DerOctetString
        return DerSequence([
            DerSequence([
                chr(0x06) + chr(len(self.oid_aesKeyWrap)) + self.oid_aesKeyWrap
            ]).encode(),
            DerOctetString(der).encode()
        ]).encode()

    def gen_salt(self, bytes = 16):
        from Crypto import Random
        return Random.new().read(bytes)

    def wrapper(self, salt, keylen = 256, iterations = 8000):
        from Crypto.Protocol.KDF import PBKDF2
        from Crypto.Hash         import SHA256, HMAC
        return AESKeyWrapWithPadding(PBKDF2(
            password = getpass.getpass("KEKEK Passphrase: "),
            salt     = salt,
            dkLen    = keylen/8,
            count    = iterations,
            prf      = lambda p, s: HMAC.new(p, s, SHA256).digest()))

    @classmethod
    def is_soft_key(cls, db):
        return all(k in db for k in ("kekek_pkcs8", "kekek_salt"))

    @classmethod
    def generate(cls, args, result):
        from Crypto.PublicKey import RSA
        self = cls()
        k = RSA.generate(args.keylen)
        salt  = self.gen_salt()
        spki  = k.publickey().exportKey(format = "DER")
        pkcs8 = self.encode_EncryptedPrivateKeyInfo(self.wrapper(salt).wrap(
            k.exportKey(format = "DER", pkcs = 8)))
        result.update(kekek_salt   = b64(salt),
                      kekek_pkcs8  = b64(pkcs8),
                      kekek_pubkey = b64(spki))

    @classmethod
    def recover(cls, db):
        self = cls()
        return self.wrapper(b64join(db["kekek_salt"])).unwrap(
            self.parse_EncryptedPrivateKeyInfo(b64join(db["kekek_pkcs8"])))


if __name__ == "__main__":
    main()