diff options
Diffstat (limited to 'base/common/python')
| -rw-r--r-- | base/common/python/pki/cert.py | 57 | ||||
| -rw-r--r-- | base/common/python/pki/cryptoutil.py | 175 | ||||
| -rw-r--r-- | base/common/python/pki/key.py | 253 | ||||
| -rw-r--r-- | base/common/python/pki/kraclient.py | 442 | ||||
| -rw-r--r-- | base/common/python/pki/systemcert.py | 46 |
5 files changed, 554 insertions, 419 deletions
diff --git a/base/common/python/pki/cert.py b/base/common/python/pki/cert.py index 6b8994e15..0a720a5e1 100644 --- a/base/common/python/pki/cert.py +++ b/base/common/python/pki/cert.py @@ -1,11 +1,13 @@ #!/usr/bin/python ''' -Created on Aug 27, 2013 +Created on Feb 13, 2014 +Note: The implementation in this file has not been completed and is not tested. +This note should be removed when testing/implementation is complete. @author: akoneru ''' import pki.client as client -import pki.encoder as e +import pki.encoder as encoder import json import types @@ -32,11 +34,13 @@ class CertData(object): ''' Constructor ''' self.Encoded = None - def from_dict(self, attr_list): + @classmethod + def from_dict(cls, attr_list): ''' Return CertData object from JSON dict ''' + cert_data = cls() for key in attr_list: - setattr(self, key, attr_list[key]) - return self + setattr(cert_data, key, attr_list[key]) + return cert_data class CertDataInfo(object): ''' @@ -58,11 +62,13 @@ class CertDataInfo(object): self.issuedOn = None self.issuedBy = None - def from_dict(self, attr_list): + @classmethod + def from_dict(cls, attr_list): ''' Return CertDataInfo object from JSON dict ''' + cert_data_info = cls() for key in attr_list: - setattr(self, key, attr_list[key]) - return self + setattr(cert_data_info, key, attr_list[key]) + return cert_data_info class CertDataInfos(object): ''' @@ -75,17 +81,17 @@ class CertDataInfos(object): self.certInfoList = [] self.links = [] - def decode_from_json(self, json_value): + @classmethod + def from_json(cls, json_value): ''' Populate object from JSON input ''' + ret = cls() cert_infos = json_value['CertDataInfo'] if not isinstance(cert_infos, types.ListType): - certInfo = CertDataInfo() - self.certInfoList.append(certInfo.from_dict(cert_infos)) + ret.certInfoList.append(CertDataInfo.from_dict(cert_infos)) else: for cert_info in cert_infos: - cert_data_info = CertDataInfo() - cert_data_info.from_dict(cert_info) - self.certInfoList.append(cert_data_info) + ret.certInfoList.append(CertDataInfo.from_dict(cert_info)) + return ret class CertSearchRequest(object): @@ -134,7 +140,7 @@ class CertSearchRequest(object): self.certTypeInUse = False -class CertResource(object): +class CertClient(object): ''' Class encapsulating and mirroring the functionality in the CertResouce Java interface class defining the REST API for Certificate resources. @@ -153,8 +159,7 @@ class CertResource(object): ''' Return a CertData object for a particular certificate. ''' url = self.cert_url + '/' + str(cert_id.id) response = self.connection.get(url, self.headers) - e.TYPES['CertData'] = CertData() - certData = e.CustomTypeDecoder(response.json()) + certData = encoder.CustomTypeDecoder(response.json()) return certData def listCerts(self, status = None): @@ -171,13 +176,10 @@ class CertResource(object): def searchCerts(self, cert_search_request): ''' Return a CertDataInfos object containing the results of a cert search.''' url = self.cert_url + '/search' - e.TYPES['CertSearchRequest'] = CertSearchRequest - searchRequest = json.dumps(cert_search_request, cls=e.CustomTypeEncoder) + searchRequest = json.dumps(cert_search_request, cls=encoder.CustomTypeEncoder) r = self.connection.post(url, searchRequest, self.headers) print r.json()['CertDataInfos'] - cdis = CertDataInfos() - cdis.decode_from_json(r.json()['CertDataInfos']) - return cdis + return CertDataInfos.from_json(r.json()['CertDataInfos']) def getCerts(self, cert_search_request): ''' Doctring needed here. ''' @@ -199,19 +201,14 @@ class CertResource(object): ''' Doc string needed here ''' pass +encoder.NOTYPES['CertData'] = CertData +encoder.NOTYPES['CertSearchRequest'] = CertSearchRequest - def get_transport_cert(self): - ''' Return transport certificate ''' - url = '/rest/config/cert/transport' - response = self.connection.get(url, self.headers) - certData = CertData() - certData.Encoded = response.json()['Encoded'] - return certData.Encoded def main(): connection = client.PKIConnection('http', 'localhost', '8080', 'ca') connection.authenticate('caadmin', 'Secret123') - certResource = CertResource(connection) + certResource = CertClient(connection) cert = certResource.getCert(CertId('0x6')) print cert diff --git a/base/common/python/pki/cryptoutil.py b/base/common/python/pki/cryptoutil.py new file mode 100644 index 000000000..e4a01e323 --- /dev/null +++ b/base/common/python/pki/cryptoutil.py @@ -0,0 +1,175 @@ +#!/usr/bin/python +# Authors: +# Ade Lee <alee@redhat.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright (C) 2013 Red Hat, Inc. +# All rights reserved. +# +''' +Module containing crypto classes. +''' +import abc +import base64 +import nss.nss as nss + + +class CryptoUtil(object): + ''' + Abstract class containing methods to do cryptographic operations. + ''' + __metaclass__ = abc.ABCMeta + + def __init__(self): + ''' Constructor ''' + pass + + @abc.abstractmethod + def generate_symmetric_key(self, mechanism=None): + ''' Generate and return a symmetric key ''' + pass + + @abc.abstractmethod + def symmetric_wrap(self, data, wrapping_key, mechanism=None, nonce_iv=None): + ''' encrypt data using a symmetric key (wrapping key)''' + pass + + @abc.abstractmethod + def symmetric_unwrap(self, data, wrapping_key, mechanism=None, nonce_iv=None): + ''' decrypt data originally encrypted with symmetric key (wrapping key) + + We expect the data and nonce_iv values to be base64 encoded. + The mechanism is the type of key used to do the wrapping. It defaults + to a 56 bit DES3 key. + ''' + pass + + @abc.abstractmethod + def asymmetric_wrap(self, data, wrapping_cert, mechanism=None): + ''' encrypt a symmetric key with the public key of a transport cert. + + The mechanism is the type of symmetric key, which defaults to a 56 bit + DES3 key. + ''' + pass + + #abc.abstractmethod + def get_cert(self, cert_nick): + ''' Get the certificate for the specified cert_nick. ''' + pass + +class NSSCryptoUtil(CryptoUtil): + ''' + Class that defines NSS implementation of CryptoUtil. + Requires an NSS database to have been set up and initialized. + ''' + + def __init__(self, certdb_dir, certdb_password): + ''' Initialize nss and nss related parameters + + This method expects a NSS database to have already been created at + certdb_dir with password certdb_password. + ''' + CryptoUtil.__init__(self) + self.certdb_dir = certdb_dir + self.certdb_password = certdb_password + nss.nss_init(certdb_dir) + self.nonce_iv = "e4:bb:3b:d3:c3:71:2e:58" + + @staticmethod + def setup_contexts(mechanism, sym_key, nonce_iv): + ''' Set up contexts to do wrapping/unwrapping by symmetric keys. ''' + # Get a PK11 slot based on the cipher + slot = nss.get_best_slot(mechanism) + + if sym_key == None: + sym_key = slot.key_gen(mechanism, None, slot.get_best_key_length(mechanism)) + + # If initialization vector was supplied use it, otherwise set it to None + if nonce_iv: + iv_data = nss.read_hex(nonce_iv) + iv_si = nss.SecItem(iv_data) + iv_param = nss.param_from_iv(mechanism, iv_si) + else: + iv_length = nss.get_iv_length(mechanism) + if iv_length > 0: + iv_data = nss.generate_random(iv_length) + iv_si = nss.SecItem(iv_data) + iv_param = nss.param_from_iv(mechanism, iv_si) + else: + iv_param = None + + # Create an encoding context + encoding_ctx = nss.create_context_by_sym_key(mechanism, nss.CKA_ENCRYPT, + sym_key, iv_param) + + # Create a decoding context + decoding_ctx = nss.create_context_by_sym_key(mechanism, nss.CKA_DECRYPT, + sym_key, iv_param) + + return encoding_ctx, decoding_ctx + + def generate_symmetric_key(self, mechanism=nss.CKM_DES3_CBC_PAD): + ''' Returns a symmetric key.''' + slot = nss.get_best_slot(mechanism) + return slot.key_gen(mechanism, None, slot.get_best_key_length(mechanism)) + + def symmetric_wrap(self, data, wrapping_key, mechanism=nss.CKM_DES3_CBC_PAD, nonce_iv=None): + ''' + :param data: Data to be wrapped + :param wrapping_key Symmetric key to wrap data + + Wrap (encrypt) data using the supplied symmetric key + ''' + encoding_ctx, _decoding_ctx = self.setup_contexts(mechanism, wrapping_key, nonce_iv) + wrapped_data = encoding_ctx.cipher_op(data) + encoding_ctx.digest_final() + return wrapped_data + + def symmetric_unwrap(self, data, wrapping_key, mechanism=nss.CKM_DES3_CBC_PAD, nonce_iv=None): + ''' + :param data: Data to be unwrapped (base 64 encoded) + :param wrapping_key Symmetric key to unwrap data + :param nonce_iv Base 64 encoded iv data + + Unwrap (decrypt) data using the supplied symmetric key + ''' + if nonce_iv == None: + nonce_iv = self.nonce_iv + else: + nonce_iv = nss.data_to_hex(base64.decodestring(nonce_iv)) + + _encoding_ctx, decoding_ctx = self.setup_contexts(mechanism, wrapping_key, nonce_iv) + unwrapped_data = decoding_ctx.cipher_op(base64.decodestring(data)) \ + + decoding_ctx.digest_final() + return unwrapped_data + + def asymmetric_wrap(self, data, wrapping_cert, mechanism=nss.CKM_DES3_CBC_PAD): + ''' + :param data: Data to be wrapped + :param wrapping_cert Public key to wrap data + :param mechanism algorithm of symmetric key to be wrapped + + Wrap (encrypt) data using the supplied asymmetric key + ''' + public_key = wrapping_cert.subject_public_key_info.public_key + return base64.b64encode(nss.pub_wrap_sym_key(mechanism, public_key, data)) + + def get_cert(self, cert_nick): + ''' + :param cert_nick Nickname for the certificate to be returned + + Searches NSS database and returns SecItem object for this certificate. + ''' + return nss.find_cert_from_nickname(cert_nick) diff --git a/base/common/python/pki/key.py b/base/common/python/pki/key.py index a55696cb3..0572ea264 100644 --- a/base/common/python/pki/key.py +++ b/base/common/python/pki/key.py @@ -20,10 +20,10 @@ # All rights reserved. # ''' -Module containing the Python client classes for the KeyResource and -KeyRequestResource REST API on a DRM +Module containing the Python client classes for the KeyClient and +KeyRequestClient REST API on a DRM ''' -import pki.encoder as e +import pki.encoder as encoder import json import types @@ -57,11 +57,13 @@ class KeyData(object): self.size = None self.wrappedPrivateData = None - def from_dict(self, attr_list): + @classmethod + def from_dict(cls, attr_list): ''' Return a KeyData object from a JSON dict ''' + key_data = cls() for key in attr_list: - setattr(self, key, attr_list[key]) - return self + setattr(key_data, key, attr_list[key]) + return key_data class KeyInfo(object): ''' @@ -79,11 +81,13 @@ class KeyInfo(object): self.ownerName = None self.size = None - def from_dict(self, attr_list): + @classmethod + def from_dict(cls, attr_list): ''' Return KeyInfo from JSON dict ''' + key_info = cls() for key in attr_list: - setattr(self, key, attr_list[key]) - return self + setattr(key_info, key, attr_list[key]) + return key_info def get_key_id(self): ''' Return the key ID as parsed from key URL ''' @@ -104,17 +108,17 @@ class KeyInfoCollection(object): self.key_infos = [] self.links = [] - def decode_from_json(self, json_value): - ''' Populate the object from its JSON representation ''' + @classmethod + def from_json(cls, json_value): + ''' Return a KeyInfoCollection object from its JSON representation ''' + ret = cls() infos = json_value['entries'] if not isinstance(infos, types.ListType): - info = KeyInfo() - self.key_infos.append(info.from_dict(infos)) + ret.key_infos.append(KeyInfo.from_dict(infos)) else: for info in infos: - key_info = KeyInfo() - key_info.from_dict(info) - self.key_infos.append(key_info) + ret.key_infos.append(KeyInfo.from_dict(info)) + return ret class KeyRequestInfo(object): ''' @@ -129,11 +133,13 @@ class KeyRequestInfo(object): self.keyURL = None self.requestStatus = None - def from_dict(self, attr_list): + @classmethod + def from_dict(cls, attr_list): ''' Return a KeyRequestInfo object from a JSON dict. ''' + key_request_info = cls() for key in attr_list: - setattr(self, key, attr_list[key]) - return self + setattr(key_request_info, key, attr_list[key]) + return key_request_info def get_request_id(self): ''' Return the request ID by parsing the request URL. ''' @@ -161,17 +167,17 @@ class KeyRequestInfoCollection(object): self.key_requests = [] self.links = [] - def decode_from_json(self, json_value): - ''' Populate the object from its JSON representation. ''' + @classmethod + def from_json(cls, json_value): + ''' Return a KeyRequestInfoCollection object from its JSON representation. ''' + ret = cls() infos = json_value['entries'] if not isinstance(infos, types.ListType): - info = KeyRequestInfo() - self.key_requests.append(info.from_dict(infos)) + ret.key_requests.append(KeyRequestInfo.from_dict(infos)) else: for info in infos: - key_request_info = KeyRequestInfo() - key_request_info.from_dict(info) - self.key_requests.append(key_request_info) + ret.key_requests.append(KeyRequestInfo.from_dict(info)) + return ret class KeyRequestResponse(object): ''' @@ -186,10 +192,26 @@ class KeyRequestResponse(object): self.requestInfo = None self.keyData = None - def decode_from_json(self, json_value): - ''' Populate the object from its JSON representation. ''' - self.requestInfo = KeyRequestInfo() - self.requestInfo.from_dict(json_value['RequestInfo']) + @classmethod + def from_json(cls, json_value): + ''' Return a KeyRequestResponse object from its JSON representation. ''' + ret = cls() + + if 'RequestInfo' in json_value: + ret.requestInfo = KeyRequestInfo.from_dict(json_value['RequestInfo']) + + if 'KeyData' in json_value: + ret.keyData = KeyData.from_dict(json_value['KeyData']) + return ret + + def get_key_id(self): + ''' Return the id for the key archived, recovered or generated ''' + return self.requestInfo.get_key_id() + + def get_request_id(self): + ''' Return the id for the created request ''' + return self.requestInfo.get_request_id() + class Attribute(object): ''' @@ -290,27 +312,33 @@ class SymKeyGenerationRequest(ResourceMessage): ENCRYPT_USAGE = "encrypt" def __init__(self, client_id=None, key_size=None, key_algorithm=None, - key_usage=None): + key_usages=None): ''' Constructor ''' ResourceMessage.__init__(self, "com.netscape.certsrv.key.SymKeyGenerationRequest") + key_usages = key_usages or [] self.add_attribute("clientID", client_id) self.add_attribute("keySize", key_size) self.add_attribute("keyAlgorithm", key_algorithm) - self.add_attribute("keyUsage", key_usage) + self.add_attribute("keyUsage", ','.join(key_usages)) -class KeyResource(object): +class KeyClient(object): ''' Class that encapsulates and mirrors the functions in the KeyResource - Java class in the DRM REST API. + and KeyRequestResource Java classes in the DRM REST API. ''' + SYMMETRIC_KEY_TYPE = "symmetricKey" + PASS_PHRASE_TYPE = "passPhrase" + ASYMMETRIC_KEY_TYPE = "asymmetricKey" + def __init__(self, connection): ''' Constructor ''' self.connection = connection self.headers = {'Content-type': 'application/json', 'Accept': 'application/json'} self.keyURL = '/rest/agent/keys' + self.keyRequestsURL = '/rest/agent/keyrequests' def list_keys(self, client_id=None, status=None, max_results=None, max_time=None, start=None, size=None): @@ -323,9 +351,7 @@ class KeyResource(object): 'maxResults':max_results, 'maxTime':max_time, 'start':start, 'size':size} response = self.connection.get(self.keyURL, self.headers, params=query_params) - kdis = KeyInfoCollection() - kdis.decode_from_json(response.json()) - return kdis + return KeyInfoCollection.from_json(response.json()) def retrieve_key(self, data): ''' Retrieve a secret from the DRM. @@ -338,29 +364,41 @@ class KeyResource(object): Returns a KeyData object containing the wrapped secret. ''' url = self.keyURL + '/retrieve' - print url - e.NOTYPES['KeyRecoveryRequest'] = KeyRecoveryRequest - e.NOTYPES['ResourceMessage'] = ResourceMessage - e.NOTYPES['Attribute'] = Attribute - e.NOTYPES['AttributeList'] = AttributeList - keyRequest = json.dumps(data, cls=e.CustomTypeEncoder, sort_keys=True) + keyRequest = json.dumps(data, cls=encoder.CustomTypeEncoder, sort_keys=True) response = self.connection.post(url, keyRequest, self.headers) - keydata = KeyData() - keydata.from_dict(response.json()) - return keydata + return KeyData.from_dict(response.json()) -class KeyRequestResource(object): - ''' - Class that encapsulates and mirrors the functions in the KeyRequestResource - Java class in the DRM REST API/ - ''' + def request_key_retrieval(self, key_id, request_id, trans_wrapped_session_key=None, + session_wrapped_passphrase=None, passphrase=None, nonce_data=None): + ''' Retrieve a secret from the DRM. - def __init__(self, connection): - ''' Constructor ''' - self.connection = connection - self.headers = {'Content-type': 'application/json', - 'Accept': 'application/json'} - self.keyRequestsURL = '/rest/agent/keyrequests' + The secret (which is referenced by key_id) can be retrieved only if the + recovery request (referenced by request_id) is approved. key_id and request_id + are required. + + Data must be provided to wrap the recovered secret. This can either be + a) a 56-bit DES3 symmetric key, wrapped by the DRM transport key, and + passed in trans_wrapped_session_key + b) a passphrase. In this case, the passphrase must be wrapped by a 56-bit + symmetric key ("the session key" and passed in session_wrapped_passphrase, + and the session key must be wrapped by the DRM transport key and passed + in trans_wrapped_session_key + c) a passphrase for a p12 file. If the key being recovered is an asymmetric + key, then it is possible to pass in the passphrase for the P12 file to + be generated. This is passed in as passphrase + + nonce_data may also be passed as a salt. + + Returns a KeyData object containing the wrapped secret. + ''' + request = KeyRecoveryRequest(key_id=key_id, + request_id=request_id, + trans_wrapped_session_key=trans_wrapped_session_key, + session_wrapped_passphrase=session_wrapped_passphrase, + nonce_data=nonce_data, + passphrase=passphrase) + + return self.retrieve_key(request) def list_requests(self, request_state=None, request_type=None, client_id=None, start=None, page_size=None, max_results=None, max_time=None): @@ -374,17 +412,13 @@ class KeyRequestResource(object): 'maxResults':max_results, 'maxTime':max_time} response = self.connection.get(self.keyRequestsURL, self.headers, params=query_params) - kdis = KeyRequestInfoCollection() - kdis.decode_from_json(response.json()) - return kdis + return KeyRequestInfoCollection.from_json(response.json()) def get_request_info(self, request_id): ''' Return a KeyRequestInfo object for a specific request. ''' - url = self.keyRequestsURL + '/' + request_id.value + url = self.keyRequestsURL + '/' + request_id response = self.connection.get(url, self.headers) - info = KeyRequestInfo() - info.from_dict(response.json()) - return info + return KeyRequestInfo.from_dict(response.json()) def create_request(self, request): ''' Submit an archival, recovery or key generation request @@ -396,63 +430,92 @@ class KeyRequestResource(object): returns a KeyRequestResponse object. ''' url = self.keyRequestsURL - print request - e.NOTYPES['SymKeyGenerationRequest'] = SymKeyGenerationRequest - e.NOTYPES['KeyArchivalRequest'] = KeyArchivalRequest - e.NOTYPES['KeyRecoveryRequest'] = KeyRecoveryRequest - e.NOTYPES['ResourceMessage'] = ResourceMessage - e.NOTYPES['Attribute'] = Attribute - e.NOTYPES['AttributeList'] = AttributeList - key_request1 = json.dumps(request, cls=e.CustomTypeEncoder, sort_keys=True) - print key_request1 - response = self.connection.post(url, key_request1, self.headers) - key_response = KeyRequestResponse() - key_response.decode_from_json(response.json()) - return key_response + key_request = json.dumps(request, cls=encoder.CustomTypeEncoder, sort_keys=True) + response = self.connection.post(url, key_request, self.headers) + return KeyRequestResponse.from_json(response.json()) def approve_request(self, request_id): ''' Approve a secret recovery request ''' - url = self.keyRequestsURL + '/' + request_id.value + '/approve' + url = self.keyRequestsURL + '/' + request_id + '/approve' return self.connection.post(url, self.headers) def reject_request(self, request_id): ''' Reject a secret recovery request. ''' - url = self.keyRequestsURL + '/' + request_id.value + '/reject' + url = self.keyRequestsURL + '/' + request_id + '/reject' return self.connection.post(url, self.headers) def cancel_request(self, request_id): ''' Cancel a secret recovery request ''' - url = self.keyRequestsURL + '/' + request_id.value + '/cancel' + url = self.keyRequestsURL + '/' + request_id + '/cancel' return self.connection.post(url, self.headers) + def request_recovery(self, key_id, request_id=None, session_wrapped_passphrase=None, + trans_wrapped_session_key=None, b64certificate=None, nonce_data=None): + ''' Create a request to recover a secret. + + To retrieve a symmetric key or passphrase, the only parameter that is required is + the keyId. It is possible (but not required) to pass in the session keys/passphrase + and nonceData for the retrieval at this time. Those parameters are documented + in the docstring for retrieve_key below. + + To retrieve an asymmetric key, the keyId and the the base-64 encoded certificate + is required. + ''' + request = KeyRecoveryRequest(key_id=key_id, + request_id=request_id, + trans_wrapped_session_key=trans_wrapped_session_key, + session_wrapped_passphrase=session_wrapped_passphrase, + certificate=b64certificate, + nonce_data=nonce_data) + return self.create_request(request) + + def request_archival(self, client_id, data_type, wrapped_private_data, + key_algorithm=None, key_size=None): + ''' Archive a secret (symmetric key or passphrase) on the DRM. + + Requires a user-supplied client ID. There can be only one active + key with a specified client ID. If a record for a duplicate active + key exists, an exception is thrown. + + data_type can be one of the following: + + wrapped_private_data consists of a PKIArchiveOptions structure, which + can be constructed using either generate_archive_options() or + generate_pki_archive_options() below. + + key_algorithm and key_size are applicable to symmetric keys only. + If a symmetric key is being archived, these parameters are required. + ''' + request = KeyArchivalRequest(client_id=client_id, + data_type=data_type, + wrapped_private_data=wrapped_private_data, + key_algorithm=key_algorithm, + key_size=key_size) + return self.create_request(request) + +encoder.NOTYPES['Attribute'] = Attribute +encoder.NOTYPES['AttributeList'] = AttributeList +encoder.NOTYPES['KeyArchivalRequest'] = KeyArchivalRequest +encoder.NOTYPES['KeyRecoveryRequest'] = KeyRecoveryRequest +encoder.NOTYPES['ResourceMessage'] = ResourceMessage +encoder.NOTYPES['SymKeyGenerationRequest'] = SymKeyGenerationRequest def main(): + ''' Some unit tests - basically printing different types of requests ''' print "printing symkey generation request" client_id = "vek 123" gen_request = SymKeyGenerationRequest(client_id, 128, "AES", "encrypt,decrypt") - e.NOTYPES['SymKeyGenerationRequest'] = SymKeyGenerationRequest - e.NOTYPES['ResourceMessage'] = ResourceMessage - e.NOTYPES['Attribute'] = Attribute - e.NOTYPES['AttributeList'] = AttributeList - print json.dumps(gen_request, cls=e.CustomTypeEncoder, sort_keys=True) + print json.dumps(gen_request, cls=encoder.CustomTypeEncoder, sort_keys=True) print "printing key recovery request" key_request = KeyRecoveryRequest("25", "MX12345BBBAAA", None, "1234ABC", None, None) - e.NOTYPES['KeyRecoveryRequest'] = KeyRecoveryRequest - e.NOTYPES['ResourceMessage'] = ResourceMessage - e.NOTYPES['Attribute'] = Attribute - e.NOTYPES['AttributeList'] = AttributeList - print json.dumps(key_request, cls=e.CustomTypeEncoder, sort_keys=True) + print json.dumps(key_request, cls=encoder.CustomTypeEncoder, sort_keys=True) print "printing key archival request" archival_request = KeyArchivalRequest(client_id, "symmetricKey", "MX123AABBCD", "AES", 128) - e.NOTYPES['KeyArchivalRequest'] = KeyArchivalRequest - e.NOTYPES['ResourceMessage'] = ResourceMessage - e.NOTYPES['Attribute'] = Attribute - e.NOTYPES['AttributeList'] = AttributeList - print json.dumps(archival_request, cls=e.CustomTypeEncoder, sort_keys=True) + print json.dumps(archival_request, cls=encoder.CustomTypeEncoder, sort_keys=True) if __name__ == '__main__': main() diff --git a/base/common/python/pki/kraclient.py b/base/common/python/pki/kraclient.py index 0cb6707ef..f2b7a5582 100644 --- a/base/common/python/pki/kraclient.py +++ b/base/common/python/pki/kraclient.py @@ -21,141 +21,144 @@ # ''' Module containing KRAClient class. This class should be used by Python clients -to interact with the DRM to expose the functionality of the KeyResource and +to interact with the DRM to expose the functionality of the KeyClient and KeyRequestResouce REST APIs. ''' -import base64 -import pki.client as client import pki.key as key -import pki.cert as cert -import nss.nss as nss -import time + +from pki.systemcert import SystemCertClient class KRAClient(object): ''' Client class that models interactions with a KRA using the Key and KeyRequest REST APIs. ''' - def __init__(self, connection): - ''' Constructor ''' + def __init__(self, connection, crypto, transport_cert_nick): + ''' Constructor + + :param connection - PKIConnection object with DRM connection info. + :param crypto - CryptoUtil object. NSSCryptoUtil is provided by default. + If a different crypto implementation is desired, a different + subclass of CryptoUtil must be provided. + :param trnasport_cert_nick - identifier for the DRM transport certificate. This will + be passed to the CryptoUtil.get_cert() command to get a representation + of the transport certificate usable for crypto operations. + ''' self.connection = connection - self.key_resource = key.KeyResource(connection) - self.key_request_resource = key.KeyRequestResource(connection) - self.cert_resource = cert.CertResource(connection) + self.keys = key.KeyClient(connection) + self.system_certs = SystemCertClient(connection) + self.crypto = crypto + self.transport_cert = crypto.get_cert(transport_cert_nick) - # nss parameters - self.certdb_dir = None - self.certdb_password = None - self.transport_nick = None - self.transport_cert = None + def retrieve_key(self, key_id, trans_wrapped_session_key=None): + ''' Retrieve a secret (passphrase or symmetric key) from the DRM. - def initialize_nss(self, certdb_dir, certdb_password, transport_nick): - ''' Initialize nss and nss related parameters + This function generates a key recovery request, approves it, and retrieves + the secret referred to by key_id. This assumes that only one approval is required + to authorize the recovery. - We expect this method to be called when an nss database is to - be used to do client side cryptographic operations. + To ensure data security in transit, the data will be returned encrypted by a session + key (56 bit DES3 symmetric key) - which is first wrapped (encrypted) by the public + key of the DRM transport certificate before being sent to the DRM. The + parameter trans_wrapped_session_key refers to this wrapped session key. - This method expects a NSS database to have already been created at - certdb_dir with password certdb_password, and the DRM transport - certificate to have been imported as transport_nick - ''' - self.certdb_dir = certdb_dir - self.certdb_password = certdb_password - self.transport_nick = transport_nick - nss.nss_init(certdb_dir) - self.transport_cert = nss.find_cert_from_nickname(self.transport_nick) - - def get_transport_cert(self): - ''' Return the b64 of the transport certificate. ''' - return self.cert_resource.get_transport_cert() - - def list_requests(self, request_state, request_type, start=0, - page_size=100, max_results=100, max_time=10): - ''' Search for a list of key requests of a specified type and state. - - The permitted values for request_state are:XXXX - The permitted values for request_type are: - - Return a list of KeyRequestInfo objects ''' - return self.key_request_resource.list_requests(request_state=request_state, - request_type=request_type, - start=start, - page_size=page_size, - max_results=max_results, - max_time=max_time) - def get_request(self, request_id): - ''' Return a KeyRequestInfo object for a specific request ''' - return self.key_request_resource.get_request_info(key.RequestId(request_id)) - - def list_keys(self, client_id, status): - ''' Search for secrets archived in the DRM with a given client ID and status. - - The permitted values for status are: active, inactive - Return a list of KeyInfo objects + There are two ways of using this function: + + 1) trans_wrapped_session_key is not provided by caller. + + In this case, the function will call CryptoUtil methods to generate and wrap the + session key. The function will return the tuple (KeyData, unwrapped_secret) + + 2) The trans_wrapped_session_key is provided by the caller. + + In this case, the function will simply pass the data to the DRM, and will return the secret + wrapped in the session key. The secret will still need to be unwrapped by the caller. + + The function will return the tuple (KeyData, None), where the KeyData structure includes the + wrapped secret and some nonce data to be used as a salt when unwrapping. ''' - return self.key_resource.list_keys(client_id, status) + key_provided = True + if (trans_wrapped_session_key == None): + key_provided = False + session_key = self.crypto.generate_symmetric_key() + trans_wrapped_session_key = self.crypto.asymmetric_wrap(session_key, + self.transport_cert) + + response = self.keys.request_recovery(key_id) + request_id = response.get_request_id() + self.keys.approve_request(request_id) + + key_data = self.keys.request_key_retrieval(key_id, request_id, + trans_wrapped_session_key=trans_wrapped_session_key) + if key_provided: + return key_data, None + + unwrapped_key = self.crypto.symmetric_unwrap(key_data.wrappedPrivateData, session_key, + iv=key_data.nonceData) + return key_data, unwrapped_key + + def retrieve_key_by_passphrase(self, key_id, passphrase=None, + trans_wrapped_session_key=None, + session_wrapped_passphrase=None, + nonce_data=None): + ''' Retrieve a secret (passphrase or symmetric key) from the DRM using a passphrase. + + This function generates a key recovery request, approves it, and retrieves + the secret referred to by key_id. This assumes that only one approval is required + to authorize the recovery. + + The secret is secured in transit by wrapping the secret with a passphrase using + PBE encryption. + + There are two ways of using this function: + + 1) A passphrase is provided by the caller. + + In this case, CryptoUtil methods will be called to create the data to securely send the + passphrase to the DRM. Basically, three pieces of data will be sent: + + - the passphrase wrapped by a 56 bit DES3 symmetric key (the session key). This + is referred to as the parameter session_wrapped_passphrase above. + + - the session key wrapped with the public key in the DRM transport certificate. This + is referred to as the trans_wrapped_session_key above. - def request_recovery(self, key_id, request_id=None, session_wrapped_passphrase=None, - trans_wrapped_session_key=None, b64certificate=None, nonce_data=None): - ''' Create a request to recover a secret. + - ivps nonce data, referred to as nonce_data - To retrieve a symmetric key or passphrase, the only parameter that is required is - the keyId. It is possible (but not required) to pass in the session keys/passphrase - and nonceData for the retrieval at this time. Those parameters are documented - in the docstring for retrieve_key below. + The function will return the tuple (KeyData, unwrapped_secret) - To retrieve an asymmetric key, the keyId and the the base-64 encoded certificate - is required. + 2) The caller provides the trans_wrapped_session_key, session_wrapped_passphrase + and nonce_data. + + In this case, the data will simply be passed to the DRM. The function will return + the secret encrypted by the passphrase using PBE Encryption. The secret will still + need to be decrypted by the caller. + + The function will return the tuple (KeyData, None) ''' - request = key.KeyRecoveryRequest(key_id=key_id, - request_id=request_id, - trans_wrapped_session_key=trans_wrapped_session_key, - session_wrapped_passphrase=session_wrapped_passphrase, - certificate=b64certificate, - nonce_data=nonce_data) - return self.key_request_resource.create_request(request) - - def approve_request(self, request_id): - ''' Approve a key recovery request ''' - return self.key_request_resource.approve_request(key.RequestId(request_id)) - - def reject_request(self, request_id): - ''' Reject a key recovery request ''' - return self.key_request_resource.reject_request(key.RequestId(request_id)) - - def cancel_request(self, request_id): - ''' Cancel a key recovery request ''' - return self.key_request_resource.cancel_request(key.RequestId(request_id)) - - def retrieve_key(self, key_id, request_id, trans_wrapped_session_key=None, - session_wrapped_passphrase=None, passphrase=None, nonce_data=None): - ''' Retrieve a secret from the DRM. - - The secret (which is referenced by key_id) can be retrieved only if the - recovery request (referenced by request_id) is approved. key_id and request_id - are required. - - Data must be provided to wrap the recovered secret. This can either be - a) a 56-bit DES3 symmetric key, wrapped by the DRM transport key, and - passed in trans_wrapped_session_key - b) a passphrase. In this case, the passphrase must be wrapped by a 56-bit - symmetric key ("the session key" and passed in session_wrapped_passphrase, - and the session key must be wrapped by the DRM transport key and passed - in trans_wrapped_session_key - c) a passphrase for a p12 file. If the key being recovered is an asymmetric - key, then it is possible to pass in the passphrase for the P12 file to - be generated. This is passed in as passphrase - - nonce_data may also be passed as a salt. + pass + + def retrieve_key_by_pkcs12(self, key_id, certificate, passphrase): + ''' Retrieve an asymmetric private key and return it as PKCS12 data. + + This function generates a key recovery request, approves it, and retrieves + the secret referred to by key_id in a PKCS12 file. This assumes that only + one approval is required to authorize the recovery. + + This function requires the following parameters: + - key_id : the ID of the key + - certificate: the certificate associated with the private key + - passphrase: A passphrase for the pkcs12 file. + + The function returns a KeyData object. ''' - request = key.KeyRecoveryRequest(key_id=key_id, - request_id=request_id, - trans_wrapped_session_key=trans_wrapped_session_key, - session_wrapped_passphrase=session_wrapped_passphrase, - nonce_data=nonce_data, - passphrase=passphrase) - return self.key_resource.retrieve_key(request) + response = self.keys.request_recovery(key_id, b64certificate=certificate) + request_id = response.get_request_id() + self.keys.approve_request(request_id) + + return self.keys.request_key_retrieval(key_id, request_id, passphrase) + def generate_sym_key(self, client_id, algorithm, size, usages): ''' Generate and archive a symmetric key on the DRM. @@ -166,32 +169,47 @@ class KRAClient(object): request = key.SymKeyGenerationRequest(client_id=client_id, key_size=size, key_algorithm=algorithm, - key_usage=usages) - return self.key_request_resource.create_request(request) + key_usages=usages) + return self.keys.create_request(request) - def archive_key(self, client_id, data_type, wrapped_private_data, + def archive_key(self, client_id, data_type, private_data=None, + wrapped_private_data=None, key_algorithm=None, key_size=None): - ''' Archive a secret (symetric key or passphrase) on the DRM. + ''' Archive a secret (symmetric key or passphrase) on the DRM. Requires a user-supplied client ID. There can be only one active key with a specified client ID. If a record for a duplicate active - key exists, an exception is thrown. + key exists, a BadRequestException is thrown. data_type can be one of the following: + KeyRequestResource.SYMMETRIC_KEY_TYPE, + KeyRequestResource.ASYMMETRIC_KEY_TYPE, + KeyRequestResource.PASS_PHRASE_TYPE + + key_algorithm and key_size are applicable to symmetric keys only. + If a symmetric key is being archived, these parameters are required. wrapped_private_data consists of a PKIArchiveOptions structure, which can be constructed using either generate_archive_options() or generate_pki_archive_options() below. - key_algorithm and key_size are applicable to symmetric keys only. - If a symmetric key is being archived, these parameters are required. + private_data is the secret that is to be archived. + + Callers must specify EITHER wrapped_private_data OR private_data. + If wrapped_private_data is specified, then this data is forwarded to the + DRM unchanged. Otherwise, the private_data is converted to a + PKIArchiveOptions structure using the functions below. + + The function returns a KeyRequestResponse object containing a KeyRequestInfo + object with details about the archival request and key archived. ''' - request = key.KeyArchivalRequest(client_id=client_id, - data_type=data_type, - wrapped_private_data=wrapped_private_data, - key_algorithm=key_algorithm, - key_size=key_size) - return self.key_request_resource.create_request(request) + if wrapped_private_data == None: + if private_data == None: + # raise BadRequestException - to be added in next patch + return None + wrapped_private_data = self.generate_archive_options(private_data) + return self.keys.request_archival(client_id, data_type, wrapped_private_data, + key_algorithm, key_size) def generate_pki_archive_options(self, trans_wrapped_session_key, session_wrapped_secret): ''' Return a PKIArchiveOptions structure for archiving a secret @@ -202,38 +220,6 @@ class KRAClient(object): ''' pass - def setup_contexts(self, mechanism, sym_key, iv_vector): - ''' Set up contexts to do wrapping/unwrapping by symmetric keys. ''' - # Get a PK11 slot based on the cipher - slot = nss.get_best_slot(mechanism) - - if sym_key == None: - sym_key = slot.key_gen(mechanism, None, slot.get_best_key_length(mechanism)) - - # If initialization vector was supplied use it, otherwise set it to None - if iv_vector: - iv_data = nss.read_hex(iv_vector) - iv_si = nss.SecItem(iv_data) - iv_param = nss.param_from_iv(mechanism, iv_si) - else: - iv_length = nss.get_iv_length(mechanism) - if iv_length > 0: - iv_data = nss.generate_random(iv_length) - iv_si = nss.SecItem(iv_data) - iv_param = nss.param_from_iv(mechanism, iv_si) - else: - iv_param = None - - # Create an encoding context - encoding_ctx = nss.create_context_by_sym_key(mechanism, nss.CKA_ENCRYPT, - sym_key, iv_param) - - # Create a decoding context - decoding_ctx = nss.create_context_by_sym_key(mechanism, nss.CKA_DECRYPT, - sym_key, iv_param) - - return encoding_ctx, decoding_ctx - def generate_archive_options(self, secret): ''' Return a PKIArchiveOptions structure for archiving a secret. @@ -246,141 +232,9 @@ class KRAClient(object): This method expects initialize_nss() to have been called previously. ''' - mechanism = nss.CKM_DES3_CBC_PAD - slot = nss.get_best_slot(mechanism) - session_key = slot.key_gen(mechanism, None, slot.get_best_key_length(mechanism)) - - public_key = self.transport_cert.subject_public_key_info.public_key - trans_wrapped_session_key = base64.b64encode(nss.pub_wrap_sym_key( - mechanism, public_key, session_key)) - - encoding_ctx, _decoding_ctx = self.setup_contexts(mechanism, session_key, None) - wrapped_secret = encoding_ctx.cipher_op(secret) + encoding_ctx.digest_final() + session_key = self.crypto.generate_symmetric_key() + trans_wrapped_session_key = self.crypto.asymmetric_wrap(session_key, self.transport_cert) + wrapped_secret = self.crypto.symmetric_wrap(secret, session_key) return self.generate_pki_archive_options(trans_wrapped_session_key, wrapped_secret) -def print_key_request(request): - ''' Prints the relevant fields of a KeyRequestInfo object ''' - print "RequestURL: " + str(request.requestURL) - print "RequestType: " + str(request.requestType) - print "RequestStatus: " + str(request.requestStatus) - print "KeyURL: " + str(request.keyURL) - -def print_key_info(key_info): - ''' Prints the relevant fields of a KeyInfo object ''' - print "Key URL: " + str(key_info.keyURL) - print "Client ID: " + str(key_info.clientID) - print "Algorithm: " + str(key_info.algorithm) - print "Status: " + str(key_info.status) - print "Owner Name: " + str(key_info.ownerName) - print "Size: " + str(key_info.size) - -def print_key_data(key_data): - ''' Prints the relevant fields of a KeyData object ''' - print "Key Algorithm: " + str(key_data.algorithm) - print "Key Size: " + str(key_data.size) - print "Nonce Data: " + str(key_data.nonceData) - print "Wrapped Private Data: " + str(key_data.wrappedPrivateData) - -def generate_symmetric_key(mechanism): - ''' generate symmetric key - to be moved to nssutil module''' - slot = nss.get_best_slot(mechanism) - return slot.key_gen(mechanism, None, slot.get_best_key_length(mechanism)) - -def trans_wrap_sym_key(transport_cert, sym_key, mechanism): - ''' wrap a sym key with a transport cert - to be moved to nsutil module''' - public_key = transport_cert.subject_public_key_info.public_key - return base64.b64encode(nss.pub_wrap_sym_key(mechanism, public_key, sym_key)) - -def barbican_encode(kraclient, client_id, algorithm, key_size, usage_string): - response = kraclient.generate_sym_key(client_id, algorithm, key_size, usage_string) - return response.requestInfo.get_key_id() - -def barbican_decode(kraclient, key_id, wrapped_session_key): - response = kraclient.request_recovery(key_id) - recovery_request_id = response.requestInfo.get_request_id() - kraclient.approve_request(recovery_request_id) - return kraclient.retrieve_key(key_id, recovery_request_id, wrapped_session_key) - -def main(): - ''' test code execution ''' - connection = client.PKIConnection('https', 'localhost', '8443', 'kra') - connection.set_authentication_cert('/tmp/temp4.pem') - kraclient = KRAClient(connection) - # Get Transport Cert - transport_cert = kraclient.get_transport_cert() - print transport_cert - - print "Now getting key request" - keyrequest = kraclient.get_request('2') - print_key_request(keyrequest) - - print "Now listing requests" - keyrequests = kraclient.list_requests('complete', 'securityDataRecovery') - print keyrequests.key_requests - for request in keyrequests.key_requests: - print_key_request(request) - - print "Now generating symkey" - client_id = "Vek #1" + time.strftime('%X %x %Z') - algorithm = "AES" - key_size = 128 - usages = [key.SymKeyGenerationRequest.DECRYPT_USAGE, key.SymKeyGenerationRequest.ENCRYPT_USAGE] - response = kraclient.generate_sym_key(client_id, algorithm, key_size, ','.join(usages)) - print_key_request(response.requestInfo) - print "Request ID is " + response.requestInfo.get_request_id() - key_id = response.requestInfo.get_key_id() - - print "Now getting key ID for clientID=\"" + client_id + "\"" - key_infos = kraclient.list_keys(client_id, "active") - for key_info in key_infos.key_infos: - print_key_info(key_info) - key_id2 = key_info.get_key_id() - if key_id == key_id2: - print "Success! The keys from generation and search match." - else: - print "Failure - key_ids for generation do not match!" - - print "Submit recovery request" - response = kraclient.request_recovery(key_id) - print response - print_key_request(response.requestInfo) - recovery_request_id = response.requestInfo.get_request_id() - - print "Approve recovery request" - print kraclient.approve_request(recovery_request_id) - - # now begins the nss specific code - # you need to have an nss database set up with the transport cert - # imported therein. - print "Retrieve key" - nss.nss_init("/tmp/drmtest/certdb") - mechanism = nss.CKM_DES3_CBC_PAD - - transport_cert = nss.find_cert_from_nickname("kra transport cert") - session_key = generate_symmetric_key(mechanism) - print session_key - wrapped_session_key = trans_wrap_sym_key(transport_cert, session_key, nss.CKM_DES_CBC_PAD) - - response = kraclient.retrieve_key(key_id, recovery_request_id, wrapped_session_key) - print_key_data(response) - - # do the above again - but this time using Barbican -like encode() and decode() functions - - # generate a symkey - client_id = "Barbican VEK #1" + time.strftime('%X %x %Z') - algorithm = "AES" - key_size = 128 - usages = [key.SymKeyGenerationRequest.DECRYPT_USAGE, key.SymKeyGenerationRequest.ENCRYPT_USAGE] - key_id = barbican_encode(kraclient, client_id, algorithm, key_size, ','.join(usages)) - print "barbican_encode() returns " + str(key_id) - - # recover the symkey - session_key = generate_symmetric_key(mechanism) - wrapped_session_key = trans_wrap_sym_key(transport_cert, session_key, nss.CKM_DES_CBC_PAD) - response = barbican_decode(kraclient, key_id, wrapped_session_key) - print "barbican_decode() returns:" - print_key_data(response) - -if __name__ == "__main__": - main() diff --git a/base/common/python/pki/systemcert.py b/base/common/python/pki/systemcert.py new file mode 100644 index 000000000..aa1cb538d --- /dev/null +++ b/base/common/python/pki/systemcert.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# Authors: +# Ade Lee <alee@redhat.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright (C) 2013 Red Hat, Inc. +# All rights reserved. +# +''' +Module containing the Python client classes for the SystemCert REST API +''' + +from pki.cert import CertData + +class SystemCertClient(object): + ''' + Class encapsulating and mirroring the functionality in the SystemCertResouce + Java interface class defining the REST API for system certificate resources. + ''' + + def __init__(self, connection): + ''' Constructor ''' + #super(PKIResource, self).__init__(connection) + self.connection = connection + self.headers = {'Content-type': 'application/json', + 'Accept': 'application/json'} + self.cert_url = '/rest/config/cert' + + def get_transport_cert(self): + ''' Return transport certificate ''' + url = self.cert_url + '/transport' + response = self.connection.get(url, self.headers) + cert_data = CertData.from_dict(response.json()) + return cert_data.Encoded |
