Source code for pyndn.security.tpm.tpm_back_end_osx

# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
#
# Copyright (C) 2017-2019 Regents of the University of California.
# Author: Jeff Thompson <[email protected]>
# Author: From ndn-cxx security https://github.com/named-data/ndn-cxx/blob/master/ndn-cxx/security/tpm/back-end-osx.cpp
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# A copy of the GNU Lesser General Public License is in the file COPYING.

"""
This module defines the TpmBackEndOsx class which extends TpmBackEnd to
implement a TPM back-end using the macOS Keychain services.
"""

import sys
from ctypes import *
if sys.platform == 'darwin':
    from pyndn.contrib.cocoapy import *
from pyndn.util.blob import Blob
from pyndn.security.security_types import KeyType
from pyndn.security.security_types import DigestAlgorithm
from pyndn.security.tpm.tpm_private_key import TpmPrivateKey
from pyndn.security.tpm.tpm_back_end import TpmBackEnd

[docs]class TpmBackEndOsx(TpmBackEnd): def __init__(self): super(TpmBackEndOsx, self).__init__() self._isTerminalMode = False
[docs] class Error(TpmBackEnd.Error): """ Create a TpmBackEndOsx.Error which extends TpmBackEnd.Error and represents a non-semantic error in the backend TPM for the macOS Keychain services. :param str message: The error message. """ def __init__(self, message): super(TpmBackEndOsx.Error, self).__init__(message)
[docs] @staticmethod def getScheme(): return "tpm-osxkeychain"
# Management
[docs] def isTerminalMode(self): """ Check if the TPM is in terminal mode. :return: True if in terminal mode. :rtype: bool """ return self._isTerminalMode
[docs] def setTerminalMode(self, isTerminal): """ Set the terminal mode of the TPM. In terminal mode, the TPM will not ask for a password from the GUI. :param bool isTerminal: True to enable terminal mode. """ osx = Osx.get() self._isTerminalMode = isTerminal osx._security.SecKeychainSetUserInteractionAllowed(not isTerminal)
# TODO: isTpmLocked # TODO: unlockTpm # Cryptographic transformation
[docs] @staticmethod def sign(key, digestAlgorithm, data): """ Use the macOS Keychain key to sign. :param c_void_p key: The macOS Keychain private key. :param digestAlgorithm: The digest algorithm. :type digestAlgorithm: int from DigestAlgorithm :param data: The input byte buffer. :type data: an array which implements the buffer protocol :return: The signature Blob, or an isNull Blob for an unrecognized digestAlgorithm. :rtype: Blob """ osx = Osx.get() signer = None dataRef = None digestSizeRef = None signature = None try: # We need a str (Python 2) or bytes (Python 3). Use Blob to convert data. dataStr = Blob(data, False).toBytes() dataRef = c_void_p(cf.CFDataCreate(None, dataStr, len(data))) error = c_void_p() signer = osx._security.SecSignTransformCreate(key, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error("Failed to create the signer") # Set the input. osx._security.SecTransformSetAttribute( signer, osx._kSecTransformInputAttributeName, dataRef, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error( "Failed to configure the input of the signer") # Set the padding type. osx._security.SecTransformSetAttribute( signer, osx._kSecPaddingKey, osx._kSecPaddingPKCS1Key, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error( "Failed to configure the padding algorithm of the signer") # Set the digest attribute. osx._security.SecTransformSetAttribute( signer, osx._kSecDigestTypeAttribute, TpmBackEndOsx._getDigestAlgorithm(digestAlgorithm), pointer(error)) if error.value != None: raise TpmBackEndOsx.Error( "Fail to configure the digest algorithm of the signer") # Set the digest size attribute. digestSizeRef = c_void_p(cf.CFNumberCreate( None, kCFNumberLongType, byref(c_long(TpmBackEndOsx._getDigestSize(digestAlgorithm))))) osx._security.SecTransformSetAttribute( signer, osx._kSecDigestLengthAttribute, digestSizeRef, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error( "Failed to configure the digest size of the signer") # Actually sign. signature = osx._security.SecTransformExecute( signer, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error( "Failed to sign the data. Try using the Keychain Access application to set the access control of the private key to \"Allow all applications to access this item\".") if signature == None: raise TpmBackEndOsx.Error("Signature is NULL!") return TpmBackEndOsx._CFDataToBlob(signature) finally: if signer != None: cf.CFRelease(signer) if dataRef != None: cf.CFRelease(dataRef) if digestSizeRef != None: cf.CFRelease(digestSizeRef) if signature != None: cf.CFRelease(signature)
[docs] @staticmethod def decrypt(key, cipherText): """ Use the macOS Keychain key to decrypt. :param c_void_p key: The macOS Keychain private key. :param cipherText: The cipher text byte buffer. :type cipherText: an array which implements the buffer protocol :return: The decrypted data. :rtype: Blob """ osx = Osx.get() dataRef = None decryptor = None output = None try: # We need a str (Python 2) or bytes (Python 3). Use Blob to convert data. dataStr = Blob(cipherText, False).toBytes() dataRef = c_void_p(cf.CFDataCreate(None, dataStr, len(cipherText))) error = c_void_p() decryptor = osx._security.SecDecryptTransformCreate(key, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error("Failed to create the decryptor") osx._security.SecTransformSetAttribute( decryptor, osx._kSecTransformInputAttributeName, dataRef, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error("Failed to configure the decryptor") osx._security.SecTransformSetAttribute( decryptor, osx._kSecPaddingKey, osx._kSecPaddingOAEPKey, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error("Failed to configure the decryptor #2") output = osx._security.SecTransformExecute(decryptor, pointer(error)) if error.value != None: raise TpmBackEndOsx.Error( "Failed to decrypt the cipherText. Try using the Keychain Access application to set the access control of the private key to \"Allow all applications to access this item\".") if output == None: raise TpmBackEndOsx.Error("The output is NULL") return TpmBackEndOsx._CFDataToBlob(output) finally: if dataRef != None: cf.CFRelease(dataRef) if decryptor != None: cf.CFRelease(decryptor) if output != None: cf.CFRelease(output)
[docs] @staticmethod def derivePublicKey(key): """ Use the macOS Keychain key to derive the public key. :param c_void_p key: The macOS Keychain private key. :return: The public key encoding Blob. :rtype: Blob """ osx = Osx.get() exportedKey = None try: exportedKey = c_void_p() res = osx._security.SecItemExport( key, osx._kSecFormatOpenSSL, 0, None, pointer(exportedKey)) if res != None: # TODO: check for errSecAuthFailed raise TpmBackEndOsx.Error( "Failed to export the private key") privateKey = TpmPrivateKey() privateKey.loadPkcs1(TpmBackEndOsx._CFDataToBlob(exportedKey)) return privateKey.derivePublicKey() finally: if exportedKey != None: cf.CFRelease(exportedKey)
def _doHasKey(self, keyName): """ A protected method to check if the key with name keyName exists in the TPM. :param Name keyName: The name of the key. :return: True if the key exists. :rtype: bool """ osx = Osx.get() keyLabel = None attrDict = None itemRef = None try: keyLabel = CFSTR(keyName.toUri()) attrDict = c_void_p(cf.CFDictionaryCreateMutable( None, 4, cf.kCFTypeDictionaryKeyCallBacks, None)) cf.CFDictionaryAddValue( attrDict, osx._kSecClass, osx._kSecClassKey) cf.CFDictionaryAddValue( attrDict, osx._kSecAttrLabel, keyLabel) cf.CFDictionaryAddValue( attrDict, osx._kSecReturnRef, osx._kCFBooleanTrue) itemRef = c_void_p() res = osx._security.SecItemCopyMatching(attrDict, pointer(itemRef)) return res == None finally: if keyLabel != None: cf.CFRelease(keyLabel) if attrDict != None: cf.CFRelease(attrDict) def _doGetKeyHandle(self, keyName): """ A protected method to get the handle of the key with name keyName. :param Name keyName: The name of the key. :return: The handle of the key, or None if the key does not exist. :rtype: TpmKeyHandle """ keyItem = self._getKey(keyName) if keyItem == None: return None return TpmKeyHandleOsx(keyItem) def _doCreateKey(self, identityName, params): """ A protected method to create a key for identityName according to params. The created key is named as: /<identityName>/[keyId]/KEY . The key name is set in the returned TpmKeyHandle. :param Name identityName: The name if the identity. :param KeyParams params: The KeyParams for creating the key. :return: The handle of the created key. :rtype: TpmKeyHandle :raises TpmBackEnd.Error: If the key cannot be created. """ osx = Osx.get() keyLabel = None attrDict = None cfKeySize = None publicKey = None try: keyType = params.getKeyType() if keyType == KeyType.RSA: keySize = params.getKeySize() elif keyType == KeyType.EC: keySize = params.getKeySize() else: raise TpmBackEndOsx.Error( "Failed to create a key pair: Unsupported key type") cfKeySize = c_void_p(cf.CFNumberCreate( None, kCFNumberIntType, byref(c_int(keySize)))) attrDict = c_void_p(cf.CFDictionaryCreateMutable( None, 2, cf.kCFTypeDictionaryKeyCallBacks, None)) cf.CFDictionaryAddValue( attrDict, osx._kSecAttrKeyType, TpmBackEndOsx._getAsymmetricKeyType(keyType)) cf.CFDictionaryAddValue( attrDict, osx._kSecAttrKeySizeInBits, cfKeySize) publicKey = c_void_p() privateKey = c_void_p() res = osx._security.SecKeyGeneratePair( attrDict, pointer(publicKey), pointer(privateKey)) if res != 0: # TODO: check for errSecAuthFailed raise TpmBackEndOsx.Error("Failed to create a key pair") keyHandle = TpmKeyHandleOsx(privateKey) TpmBackEnd.setKeyName(keyHandle, identityName, params) keyUri = keyHandle.getKeyName().toUri() # There is only one attr, so we don't need to make a C array. attr = SecKeychainAttribute( osx._kSecKeyPrintName, len(keyUri), keyUri.encode('utf-8')) attrList = SecKeychainAttributeList(1, pointer(attr)) osx._security.SecKeychainItemModifyAttributesAndData( privateKey, byref(attrList), 0, None) osx._security.SecKeychainItemModifyAttributesAndData( publicKey, byref(attrList), 0, None) return keyHandle finally: if keyLabel != None: cf.CFRelease(keyLabel) if attrDict != None: cf.CFRelease(attrDict) if cfKeySize != None: cf.CFRelease(cfKeySize) if publicKey != None: cf.CFRelease(publicKey) def _doDeleteKey(self, keyName): """ A protected method to delete the key with name keyName. If the key doesn't exist, do nothing. :param Name keyName: The name of the key to delete. :raises TpmBackEnd.Error: If the deletion fails. """ osx = Osx.get() keyLabel = None searchDict = None try: keyLabel = CFSTR(keyName.toUri()) searchDict = c_void_p(cf.CFDictionaryCreateMutable( None, 5, cf.kCFTypeDictionaryKeyCallBacks, None)) cf.CFDictionaryAddValue( searchDict, osx._kSecClass, osx._kSecClassKey) cf.CFDictionaryAddValue( searchDict, osx._kSecAttrLabel, keyLabel) cf.CFDictionaryAddValue( searchDict, osx._kSecMatchLimit, osx._kSecMatchLimitAll) res = osx._security.SecItemDelete(searchDict) # TODO: check for errSecAuthFailed # TODO: check for errSecItemNotFound finally: if keyLabel != None: cf.CFRelease(keyLabel) if searchDict != None: cf.CFRelease(searchDict) def _getKey(self, keyName): """ Get a key from the Keychain. :param Name keyName: The name of the key. :return: None if not found, otherwise a Keychain ref to the key. You must use cf.CFRelease to free it. :rtype: c_void_p """ osx = Osx.get() keyLabel = None attrDict = None try: keyLabel = CFSTR(keyName.toUri()) attrDict = c_void_p(cf.CFDictionaryCreateMutable( None, 5, cf.kCFTypeDictionaryKeyCallBacks, None)) cf.CFDictionaryAddValue( attrDict, osx._kSecClass, osx._kSecClassKey) cf.CFDictionaryAddValue( attrDict, osx._kSecAttrLabel, keyLabel) cf.CFDictionaryAddValue( attrDict, osx._kSecAttrKeyClass, osx._kSecAttrKeyClassPrivate) cf.CFDictionaryAddValue( attrDict, osx._kSecReturnRef, osx._kCFBooleanTrue) keyItem = c_void_p() res = osx._security.SecItemCopyMatching(attrDict, pointer(keyItem)) if res != None: # TODO: check for errSecAuthFailed return None return keyItem finally: if keyLabel != None: cf.CFRelease(keyLabel) if attrDict != None: cf.CFRelease(attrDict) @staticmethod def _getAsymmetricKeyType(keyType): """ Convert keyType to the MAC OS asymmetric key key type. :param keyType: The type of the key. :type keyType: int from KeyType :return: The MAC OS key type. :rtype: c_void_p """ osx = Osx.get() if keyType == KeyType.RSA: return osx._kSecAttrKeyTypeRSA elif keyType == KeyType.EC: return osx._kSecAttrKeyTypeECDSA else: return None @staticmethod def _getDigestAlgorithm(digestAlgorithm): """ Convert digestAlgorithm to the MAC OS algorithm in. :param digestAlgorithm: The digest algorithm. :type digestAlgorithm: int from DigestAlgorithm :return: The MAC OS algorithm id. :rtype: c_void_p """ osx = Osx.get() if digestAlgorithm == DigestAlgorithm.SHA256: return osx._kSecDigestSHA2 else: return None @staticmethod def _getDigestSize(digestAlgorithm): """ Get the digest size of the corresponding algorithm :param digestAlgorithm: The digest algorithm. :type digestAlgorithm: int from DigestAlgorithm :return: The digest size. :rtype: int """ if digestAlgorithm == DigestAlgorithm.SHA256: return 256 else: return -1 @staticmethod def _CFDataToBlob(cfData): length = cf.CFDataGetLength(cfData) array = (c_byte * length)() cf.CFDataGetBytes(cfData, CFRange(0, length), array) # Convert from signed byte to unsigned byte. unsignedArray = [(x if x >= 0 else x + 256) for x in array] return Blob(unsignedArray, False)
[docs]class SecKeychainAttribute(Structure): _fields_ = [("tag", c_uint32), ("length", c_uint32), ("data", c_char_p)]
[docs]class SecKeychainAttributeList(Structure): _fields_ = [("count", c_int), ("attr", POINTER(SecKeychainAttribute))]
[docs]class Osx(object): _instance = None def __init__(self): #pylint: disable=E1103 self._kCFBooleanTrue = c_void_p.in_dll(cf, "kCFBooleanTrue") self._security = cdll.LoadLibrary( "/System/Library/Frameworks/Security.framework/Versions/Current/Security") self._security.SecItemCopyMatching.restype = c_void_p self._security.SecItemCopyMatching.argtypes = [c_void_p, POINTER(c_void_p)] self._security.SecSignTransformCreate.restype = c_void_p self._security.SecSignTransformCreate.argtypes = [c_void_p, POINTER(c_void_p)] self._security.SecDecryptTransformCreate.restype = c_void_p self._security.SecDecryptTransformCreate.argtypes = [c_void_p, POINTER(c_void_p)] self._security.SecTransformSetAttribute.restype = c_void_p self._security.SecTransformSetAttribute.argtypes = [c_void_p, c_void_p, c_void_p, POINTER(c_void_p)] self._security.SecTransformExecute.restype = c_void_p self._security.SecTransformExecute.argtypes = [c_void_p, POINTER(c_void_p)] self._security.SecItemExport.restype = c_void_p self._security.SecItemExport.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p, POINTER(c_void_p)] self._security.SecKeychainSetUserInteractionAllowed.restype = c_void_p self._security.SecKeychainSetUserInteractionAllowed.argtypes = [c_void_p] self._security.SecKeychainItemModifyAttributesAndData.restype = c_void_p self._security.SecKeychainItemModifyAttributesAndData.argtypes = [ c_void_p, POINTER(SecKeychainAttributeList), c_uint32, c_void_p] self._kSecClass = c_void_p.in_dll(self._security, "kSecClass") self._kSecClassKey = c_void_p.in_dll(self._security, "kSecClassKey") self._kSecAttrKeyType = c_void_p.in_dll(self._security, "kSecAttrKeyType") self._kSecAttrKeySizeInBits = c_void_p.in_dll(self._security, "kSecAttrKeySizeInBits") self._kSecAttrLabel = c_void_p.in_dll(self._security, "kSecAttrLabel") self._kSecAttrKeyClass = c_void_p.in_dll(self._security, "kSecAttrKeyClass") self._kSecReturnRef = c_void_p.in_dll(self._security, "kSecReturnRef") self._kSecMatchLimit = c_void_p.in_dll(self._security, "kSecMatchLimit") self._kSecMatchLimitAll = c_void_p.in_dll(self._security, "kSecMatchLimitAll") self._kSecAttrKeyTypeAES = c_void_p.in_dll(self._security, "kSecAttrKeyTypeAES") self._kSecAttrKeyTypeRSA = c_void_p.in_dll(self._security, "kSecAttrKeyTypeRSA") self._kSecAttrKeyTypeECDSA = c_void_p.in_dll(self._security, "kSecAttrKeyTypeECDSA") self._kSecAttrKeyClassPrivate = c_void_p.in_dll(self._security, "kSecAttrKeyClassPrivate") self._kSecAttrKeyClassPublic = c_void_p.in_dll(self._security, "kSecAttrKeyClassPublic") self._kSecAttrKeyClassSymmetric = c_void_p.in_dll(self._security, "kSecAttrKeyClassSymmetric") self._kSecDigestSHA2 = c_void_p.in_dll(self._security, "kSecDigestSHA2") self._kSecTransformInputAttributeName = c_void_p.in_dll(self._security, "kSecTransformInputAttributeName") self._kSecDigestTypeAttribute = c_void_p.in_dll(self._security, "kSecDigestTypeAttribute") self._kSecDigestLengthAttribute = c_void_p.in_dll(self._security, "kSecDigestLengthAttribute") self._kSecPaddingKey = c_void_p.in_dll(self._security, "kSecPaddingKey") self._kSecPaddingPKCS1Key = c_void_p.in_dll(self._security, "kSecPaddingPKCS1Key") self._kSecPaddingOAEPKey = c_void_p.in_dll(self._security, "kSecPaddingOAEPKey") self._kSecKeyPrintName = 1 #pylint: enable=E1103 # enum values: self._kSecFormatOpenSSL = 1
[docs] @staticmethod def get(): """ Get the static instance of Osx, creating it only when needed. """ if Osx._instance == None: Osx._instance = Osx() return Osx._instance
# Put this last to avoid an import loop. from pyndn.security.tpm.tpm_key_handle_osx import TpmKeyHandleOsx