diff options
author | Ade Lee <alee@redhat.com> | 2014-02-12 11:44:47 -0500 |
---|---|---|
committer | Ade Lee <alee@redhat.com> | 2014-02-19 10:48:06 -0500 |
commit | a9c460f532a5f9697b56aa116c3df772b0fd27e9 (patch) | |
tree | 8c979fc91e121ab6fbade87ac506807723bb10c6 | |
parent | 03f688d8becf32748711a5832af1a736e838905d (diff) | |
download | pki-a9c460f532a5f9697b56aa116c3df772b0fd27e9.tar.gz pki-a9c460f532a5f9697b56aa116c3df772b0fd27e9.tar.xz pki-a9c460f532a5f9697b56aa116c3df772b0fd27e9.zip |
Initial work on python API
This patch includes code for most of the python client library
for the KeyResource and KeyRequestResource for the DRM.
Some place holder code has been added for the CertResource, but this
needs to be further refined and tested.
-rw-r--r-- | .pydevproject | 1 | ||||
-rw-r--r-- | base/common/python/pki/cert.py | 219 | ||||
-rw-r--r-- | base/common/python/pki/client.py | 28 | ||||
-rw-r--r-- | base/common/python/pki/key.py | 458 | ||||
-rw-r--r-- | base/common/python/pki/kraclient.py | 386 | ||||
-rw-r--r-- | base/kra/functional/drmclient.py | 20 |
6 files changed, 1097 insertions, 15 deletions
diff --git a/.pydevproject b/.pydevproject index 4a8d2616b..fdc8738e1 100644 --- a/.pydevproject +++ b/.pydevproject @@ -6,5 +6,6 @@ <path>/pki/base/common/python/pki</path> <path>/pki/base/server/python/pki/server</path> <path>/pki/base/kra/functional</path> +<path>/pki</path> </pydev_pathproperty> </pydev_project> diff --git a/base/common/python/pki/cert.py b/base/common/python/pki/cert.py new file mode 100644 index 000000000..6b8994e15 --- /dev/null +++ b/base/common/python/pki/cert.py @@ -0,0 +1,219 @@ +#!/usr/bin/python +''' +Created on Aug 27, 2013 + +@author: akoneru +''' +import pki.client as client +import pki.encoder as e +import json +import types + +class CertId(object): + ''' + Class encapsulating a certificate serial number + ''' + + def __init__(self, cert_id): + ''' Constructor ''' + if str(cert_id).startswith('0x'): + #hex number + print 'hex number' + self.id = cert_id + else: + self.id = cert_id + +class CertData(object): + ''' + Class containing certificate data as returned from getCert() + ''' + + def __init__(self): + ''' Constructor ''' + self.Encoded = None + + def from_dict(self, attr_list): + ''' Return CertData object from JSON dict ''' + for key in attr_list: + setattr(self, key, attr_list[key]) + return self + +class CertDataInfo(object): + ''' + Class containing information contained in a CertRecord on the CA. + This data is returned when searching/listing certificate records. + ''' + + def __init__(self): + ''' Constructor ''' + self.certId = None + self.subjectDN = None + self.status = None + self.type = None + self.version = None + self.keyAlgorithmOID = None + self.keyLength = None + self.notValidBefore = None + self.notValidAfter = None + self.issuedOn = None + self.issuedBy = None + + def from_dict(self, attr_list): + ''' Return CertDataInfo object from JSON dict ''' + for key in attr_list: + setattr(self, key, attr_list[key]) + return self + +class CertDataInfos(object): + ''' + Class containing lists of CertDataInfo objects. + This data is returned when searching/listing certificate records on the CA. + ''' + + def __init__(self): + ''' Constructor ''' + self.certInfoList = [] + self.links = [] + + def decode_from_json(self, json_value): + ''' Populate object from JSON input ''' + cert_infos = json_value['CertDataInfo'] + if not isinstance(cert_infos, types.ListType): + certInfo = CertDataInfo() + self.certInfoList.append(certInfo.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) + +class CertSearchRequest(object): + + def __init__(self): + self.serialNumberRangeInUse = False + self.serialTo = None + self.serialFrom = None + self.subjectInUse = False + self.eMail = None + self.commonName = None + self.userID = None + self.orgUnit = None + self.org = None + self.locality = None + self.state = None + self.country = None + self.matchExactly = None + self.status = None + self.revokedBy = None + self.revokedOnFrom = None + self.revokedOnTo = None + self.revocationReason = None + self.issuedBy = None + self.issuedOnFrom = None + self.issuedOnTo = None + self.validNotBeforeFrom = None + self.validNotBeforeTo = None + self.validNotAfterFrom = None + self.validNotAfterTo = None + self.validityOperation = None + self.validityCount = None + self.validityUnit = None + self.certTypeSubEmailCA = None + self.certTypeSubSSLCA = None + self.certTypeSecureEmail = None + self.certTypeSSLClient = None + self.certTypeSSLServer = None + self.revokedByInUse = False + self.revokedOnInUse = False + self.revocationReasonInUse = None + self.issuedByInUse = False + self.issuedOnInUse = False + self.validNotBeforeInUse = False + self.validNotAfterInUse = False + self.validityLengthInUse = False + self.certTypeInUse = False + + +class CertResource(object): + ''' + Class encapsulating and mirroring the functionality in the CertResouce Java interface class + defining the REST API for 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/certs' + self.agent_cert_url = '/rest/agent/certs' + + def getCert(self, cert_id): + ''' 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()) + return certData + + def listCerts(self, status = None): + ''' Return a CertDataInfos object for specific certs + Not sure I understand what this method is for. + ''' + if status is not None: + cert_search_request = CertSearchRequest() + cert_search_request.status = status + + response = self.connection.get(self.cert_url, self.headers) + print response.json() + + 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) + r = self.connection.post(url, searchRequest, self.headers) + print r.json()['CertDataInfos'] + cdis = CertDataInfos() + cdis.decode_from_json(r.json()['CertDataInfos']) + return cdis + + def getCerts(self, cert_search_request): + ''' Doctring needed here. ''' + pass + + def reviewCert(self, cert_id): + ''' Doc string needed here. ''' + pass + + def revokeCert(self, cert_id, cert_revoke_request): + ''' Doc string needed here ''' + pass + + def revokeCACert(self, cert_id, cert_revoke_request): + ''' Doc string needed here. ''' + pass + + def unrevokecert(self, cert_id, cert_unrevoke_request): + ''' Doc string needed here ''' + pass + + + 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) + cert = certResource.getCert(CertId('0x6')) + print cert + +if __name__ == "__main__": + main() diff --git a/base/common/python/pki/client.py b/base/common/python/pki/client.py index 00343bb7c..c95cb2927 100644 --- a/base/common/python/pki/client.py +++ b/base/common/python/pki/client.py @@ -26,7 +26,7 @@ class PKIConnection: def __init__(self, protocol='http', hostname='localhost', - port=80, + port='8080', subsystem='ca', accept='application/json'): @@ -47,19 +47,37 @@ class PKIConnection: if username is not None and password is not None: self.session.auth = (username, password) - def get(self, path, headers=None): + def set_authentication_cert(self, pem_cert_path): + if pem_cert_path is None: + raise Exception("No path for the certificate specified.") + if len(str(pem_cert_path)) == 0: + raise Exception("No path for the certificate specified.") + self.session.cert = pem_cert_path + + def get(self, path, headers=None, params=None): r = self.session.get( self.serverURI + path, verify=False, - headers=headers) + headers=headers, + params=params) r.raise_for_status() return r - def post(self, path, payload, headers=None): + def post(self, path, payload, headers=None, params=None): r = self.session.post( self.serverURI + path, verify=False, data=payload, - headers=headers) + headers=headers, + params=params) r.raise_for_status() return r +def main(): + conn = PKIConnection() + headers = {'Content-type': 'application/json', + 'Accept': 'application/json'} + conn.set_authentication_cert('/root/temp4.pem') + print conn.get("", headers).json() + +if __name__ == "__main__": + main() diff --git a/base/common/python/pki/key.py b/base/common/python/pki/key.py new file mode 100644 index 000000000..a55696cb3 --- /dev/null +++ b/base/common/python/pki/key.py @@ -0,0 +1,458 @@ +#!/usr/bin/python +# Authors: +# Abhishek Koneru <akoneru@redhat.com> +# 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 KeyResource and +KeyRequestResource REST API on a DRM +''' +import pki.encoder as e +import json +import types + +class KeyId(object): + ''' + Class representing a key ID + ''' + def __init__(self, key_id=None): + ''' Constructor ''' + self.value = key_id + +#should be moved to request.py +class RequestId(object): + ''' + Class representing a Request ID + ''' + def __init__(self, req_id): + ''' Constructor''' + self.value = req_id + +class KeyData(object): + ''' + This is the object that contains the wrapped secret + when that secret is retrieved. + ''' + + def __init__(self): + ''' Constructor ''' + self.algorithm = None + self.nonceData = None + self.size = None + self.wrappedPrivateData = None + + def from_dict(self, attr_list): + ''' Return a KeyData object from a JSON dict ''' + for key in attr_list: + setattr(self, key, attr_list[key]) + return self + +class KeyInfo(object): + ''' + This is the object that contains information stored + in the databse record for an archived secret. It does not + contain the secret itself. + ''' + + def __init__(self): + ''' Constructor ''' + self.clientID = None + self.keyURL = None + self.algorithm = None + self.status = None + self.ownerName = None + self.size = None + + def from_dict(self, attr_list): + ''' Return KeyInfo from JSON dict ''' + for key in attr_list: + setattr(self, key, attr_list[key]) + return self + + def get_key_id(self): + ''' Return the key ID as parsed from key URL ''' + if self.keyURL != None: + indx = str(self.keyURL).rfind("/") + 1 + return str(self.keyURL)[indx:] + return None + + +class KeyInfoCollection(object): + ''' + This class represents data returned when searching the DRM archived + secrets. Essentially, its a list of KeyInfo objects. + ''' + + def __init__(self): + ''' Constructor ''' + self.key_infos = [] + self.links = [] + + def decode_from_json(self, json_value): + ''' Populate the object from its JSON representation ''' + infos = json_value['entries'] + if not isinstance(infos, types.ListType): + info = KeyInfo() + self.key_infos.append(info.from_dict(infos)) + else: + for info in infos: + key_info = KeyInfo() + key_info.from_dict(info) + self.key_infos.append(key_info) + +class KeyRequestInfo(object): + ''' + This class represents data about key requests (archival, recovery, + key generation etc.) in the DRM. + ''' + + def __init__(self): + ''' Constructor ''' + self.requestURL = None + self.requestType = None + self.keyURL = None + self.requestStatus = None + + def from_dict(self, attr_list): + ''' Return a KeyRequestInfo object from a JSON dict. ''' + for key in attr_list: + setattr(self, key, attr_list[key]) + return self + + def get_request_id(self): + ''' Return the request ID by parsing the request URL. ''' + if self.requestURL != None: + indx = str(self.requestURL).rfind("/") + 1 + return str(self.requestURL)[indx:] + return None + + def get_key_id(self): + ''' Return the ID of the secret referred to by this request. ''' + if self.keyURL != None: + indx = str(self.keyURL).rfind("/") + 1 + return str(self.keyURL)[indx:] + return None + +class KeyRequestInfoCollection(object): + ''' + This class represents the data returned when searching the key + requests in the DRM. Essentially, its a list of KeyRequestInfo + objects. + ''' + + def __init__(self): + ''' Constructor ''' + self.key_requests = [] + self.links = [] + + def decode_from_json(self, json_value): + ''' Populate the object from its JSON representation. ''' + infos = json_value['entries'] + if not isinstance(infos, types.ListType): + info = KeyRequestInfo() + self.key_requests.append(info.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) + +class KeyRequestResponse(object): + ''' + This class is returned when an archival, recovery or key generation + request is created. It includes a KeyRequestInfo object with + information about the created request, and a KeyData structure + which contains the wrapped secret (if that operation is supported). + ''' + + def __init__(self): + ''' Constructor ''' + 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']) + +class Attribute(object): + ''' + Class representing a key/value pair. + + This object is the basis of the representation of a ResourceMessage. + ''' + + def __init__(self, name, value): + ''' Constructor ''' + self.name = name + self.value = value + +class AttributeList(object): + ''' + Class representing a list of attributes. + + This class is needed because of a JavaMapper used in the REST API. + ''' + + def __init__(self): + ''' Constructor ''' + self.Attribute = [] + +class ResourceMessage(object): + ''' + This class is the basis for the various types of key requests. + It is essentially a list of attributes. + ''' + + def __init__(self, class_name): + ''' Constructor ''' + self.Attributes = AttributeList() + self.ClassName = class_name + + def add_attribute(self, name, value): + ''' Add an attribute to the list. ''' + attr = Attribute(name, value) + self.Attributes.Attribute.append(attr) + + def get_attribute_value(self, name): + ''' Get the value of a given attribute ''' + for attr in self.Attributes.Attribute: + if attr.name == name: + return attr.value + return None + +class KeyArchivalRequest(ResourceMessage): + ''' + Class representing the object sent to the DRM when archiving a secret. + ''' + + def __init__(self, client_id=None, data_type=None, wrapped_private_data=None, + key_algorithm=None, key_size=None): + ''' Constructor ''' + ResourceMessage.__init__(self, + "com.netscape.certsrv.key.KeyArchivalRequest") + self.add_attribute("clientID", client_id) + self.add_attribute("dataType", data_type) + self.add_attribute("wrappedPrivateData", wrapped_private_data) + self.add_attribute("keyAlgorithm", key_algorithm) + self.add_attribute("keySize", key_size) + +class KeyRecoveryRequest(ResourceMessage): + ''' + Class representing the data sent to the DRM when either creating a request + for the recovery of a secret, or, once the request is approved, retrieving + the secret. + ''' + + def __init__(self, key_id=None, request_id=None, + trans_wrapped_session_key=None, + session_wrapped_passphrase=None, + nonce_data=None, certificate=None, + passphrase=None): + ''' Constructor ''' + ResourceMessage.__init__(self, + "com.netscape.certsrv.key.KeyRecoveryRequest") + self.add_attribute("requestId", request_id) + self.add_attribute("transWrappedSessionKey", trans_wrapped_session_key) + self.add_attribute("sessionWrappedPassphrase", session_wrapped_passphrase) + self.add_attribute("nonceData", nonce_data) + self.add_attribute("certificate", certificate) + self.add_attribute("passphrase", passphrase) + self.add_attribute("keyId", key_id) + +class SymKeyGenerationRequest(ResourceMessage): + ''' + Class representing the data sent to the DRM when generating and archiving + a symmetric key on the DRM. + ''' + + UWRAP_USAGE = "unwrap" + WRAP_USAGE = "wrap" + VERIFY_USAGE = "verify" + SIGN_USAGE = "sign" + DECRYPT_USAGE = "decrypt" + ENCRYPT_USAGE = "encrypt" + + def __init__(self, client_id=None, key_size=None, key_algorithm=None, + key_usage=None): + ''' Constructor ''' + ResourceMessage.__init__(self, + "com.netscape.certsrv.key.SymKeyGenerationRequest") + self.add_attribute("clientID", client_id) + self.add_attribute("keySize", key_size) + self.add_attribute("keyAlgorithm", key_algorithm) + self.add_attribute("keyUsage", key_usage) + +class KeyResource(object): + ''' + Class that encapsulates and mirrors the functions in the KeyResource + Java class in the DRM REST API. + ''' + + def __init__(self, connection): + ''' Constructor ''' + self.connection = connection + self.headers = {'Content-type': 'application/json', + 'Accept': 'application/json'} + self.keyURL = '/rest/agent/keys' + + def list_keys(self, client_id=None, status=None, max_results=None, + max_time=None, start=None, size=None): + ''' List/Search archived secrets in the DRM. + + See KRAClient.list_keys for the valid values of status. + Returns a KeyInfoCollection object. + ''' + query_params = {'clientID':client_id, 'status':status, + '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 + + def retrieve_key(self, data): + ''' Retrieve a secret from the DRM. + + @param: data - a KeyRecoveryRequest containing the keyId of the + secret being retrieved, the request_id of the approved recovery + request and a wrapping mechanism. More details at + KRAClient.retrieve_key. + + 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) + response = self.connection.post(url, keyRequest, self.headers) + keydata = KeyData() + keydata.from_dict(response.json()) + return keydata + +class KeyRequestResource(object): + ''' + Class that encapsulates and mirrors the functions in the KeyRequestResource + Java class in the DRM REST API/ + ''' + + def __init__(self, connection): + ''' Constructor ''' + self.connection = connection + self.headers = {'Content-type': 'application/json', + 'Accept': 'application/json'} + self.keyRequestsURL = '/rest/agent/keyrequests' + + def list_requests(self, request_state=None, request_type=None, client_id=None, + start=None, page_size=None, max_results=None, max_time=None): + ''' List/Search key requests in the DRM. + + See KRAClient.list_requests for the valid values of request_state and + request_type. Returns a KeyRequestInfoCollection object. + ''' + query_params = {'requestState':request_state, 'requestType':request_type, + 'clientID':client_id, 'start':start, 'pageSize':page_size, + '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 + + def get_request_info(self, request_id): + ''' Return a KeyRequestInfo object for a specific request. ''' + url = self.keyRequestsURL + '/' + request_id.value + response = self.connection.get(url, self.headers) + info = KeyRequestInfo() + info.from_dict(response.json()) + return info + + def create_request(self, request): + ''' Submit an archival, recovery or key generation request + to the DRM. + + @param request - is either a KeyArchivalRequest, + KeyRecoverRequest or SymKeyGenerationRequest. + + 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 + + def approve_request(self, request_id): + ''' Approve a secret recovery request ''' + url = self.keyRequestsURL + '/' + request_id.value + '/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' + 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' + return self.connection.post(url, self.headers) + + +def main(): + 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 "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 "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) + +if __name__ == '__main__': + main() diff --git a/base/common/python/pki/kraclient.py b/base/common/python/pki/kraclient.py new file mode 100644 index 000000000..0cb6707ef --- /dev/null +++ b/base/common/python/pki/kraclient.py @@ -0,0 +1,386 @@ +#!/usr/bin/python +# Authors: +# Abhishek Koneru <akoneru@redhat.com> +# 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 KRAClient class. This class should be used by Python clients +to interact with the DRM to expose the functionality of the KeyResource 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 + +class KRAClient(object): + ''' + Client class that models interactions with a KRA using the Key and KeyRequest REST APIs. + ''' + + def __init__(self, connection): + ''' Constructor ''' + self.connection = connection + self.key_resource = key.KeyResource(connection) + self.key_request_resource = key.KeyRequestResource(connection) + self.cert_resource = cert.CertResource(connection) + + # nss parameters + self.certdb_dir = None + self.certdb_password = None + self.transport_nick = None + self.transport_cert = None + + def initialize_nss(self, certdb_dir, certdb_password, transport_nick): + ''' Initialize nss and nss related parameters + + We expect this method to be called when an nss database is to + be used to do client side cryptographic operations. + + 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 + ''' + return self.key_resource.list_keys(client_id, status) + + 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 = 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. + ''' + 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) + + def generate_sym_key(self, client_id, algorithm, size, usages): + ''' Generate and archive a symmetric key on the DRM. + + Return a KeyRequestResponse which contains a KeyRequestInfo + object that describes the URL for the request and generated key. + ''' + request = key.SymKeyGenerationRequest(client_id=client_id, + key_size=size, + key_algorithm=algorithm, + key_usage=usages) + return self.key_request_resource.create_request(request) + + def archive_key(self, client_id, data_type, wrapped_private_data, + key_algorithm=None, key_size=None): + ''' Archive a secret (symetric 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 = 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) + + def generate_pki_archive_options(self, trans_wrapped_session_key, session_wrapped_secret): + ''' Return a PKIArchiveOptions structure for archiving a secret + + Takes in a session key wrapped by the DRM transport certificate, + and a secret wrapped with the session key and creates a PKIArchiveOptions + structure to be used when archiving a secret + ''' + 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. + + This method uses NSS calls to do the following: + 1) generate a session key + 2) wrap the session key with the transport key + 3) wrap the secret with the session key + 4) create the PKIArchiveOptions structure using the results of + (2) and (3) + + 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() + + 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/kra/functional/drmclient.py b/base/kra/functional/drmclient.py index 6e3a2ac61..588b6088b 100644 --- a/base/kra/functional/drmclient.py +++ b/base/kra/functional/drmclient.py @@ -503,7 +503,7 @@ class kra: Wrap (encrypt) data using the supplied symmetric key """ - encoding_ctx, decoding_ctx = self.setup_contexts(self.mechanism, wrapping_key, self.iv) + encoding_ctx, _decoding_ctx = self.setup_contexts(self.mechanism, wrapping_key, self.iv) wrapped_data = encoding_ctx.cipher_op(data) + encoding_ctx.digest_final() return wrapped_data @@ -526,7 +526,7 @@ class kra: """ if iv == None: iv = self.iv - encoding_ctx, decoding_ctx = self.setup_contexts(self.mechanism, wrapping_key, iv) + _encoding_ctx, decoding_ctx = self.setup_contexts(self.mechanism, wrapping_key, iv) unwrapped_data = decoding_ctx.cipher_op(data) + decoding_ctx.digest_final() return unwrapped_data @@ -615,7 +615,7 @@ class kra: request = self.create_archival_request(client_id, security_data, data_type) # Call CMS - http_status, http_reason_phrase, http_headers, http_body = \ + http_status, http_reason_phrase, _http_headers, http_body = \ self._request('/kra/rest/agent/keyrequests/archive', self.kra_agent_port, self.POST, @@ -678,7 +678,7 @@ class kra: get_args = get_args + "&start=" + quote_plus(next_id) # Call CMS - http_status, http_reason_phrase, http_headers, http_body = \ + http_status, http_reason_phrase, _http_headers, http_body = \ self._request('/kra/rest/agent/keys', self.kra_agent_port, self.GET, @@ -720,7 +720,7 @@ class kra: get_args = get_args + "&start=" + quote_plus(next_id) # Call CMS - http_status, http_reason_phrase, http_headers, http_body = \ + http_status, http_reason_phrase, _http_headers, http_body = \ self._request('/kra/rest/agent/keyrequests', self.kra_agent_port, self.GET, @@ -753,7 +753,7 @@ class kra: request = self.create_recovery_request(key_id, None, None, None) # Call CMS - http_status, http_reason_phrase, http_headers, http_body = \ + http_status, http_reason_phrase, _http_headers, http_body = \ self._request('/kra/rest/agent/keyrequests/recover', self.kra_agent_port, self.POST, @@ -801,7 +801,7 @@ class kra: raise CertificateOperationError(error=_('Bad argument to approve_recovery_request')) # Call CMS - http_status, http_reason_phrase, http_headers, http_body = \ + http_status, http_reason_phrase, _http_headers, _http_body = \ self._request('/kra/rest/agent/keyrequests/' + request_id + '/approve', self.kra_agent_port, self.POST, @@ -823,7 +823,7 @@ class kra: raise CertificateOperationError(error=_('Bad argument to reject_recovery_request')) # Call CMS - http_status, http_reason_phrase, http_headers, http_body = \ + http_status, http_reason_phrase, _http_headers, _http_body = \ self._request('/kra/rest/agent/keyrequests/' + request_id + '/reject', self.kra_agent_port, self.POST, @@ -845,7 +845,7 @@ class kra: raise CertificateOperationError(error=_('Bad argument to cancel_recovery_request')) # Call CMS - http_status, http_reason_phrase, http_headers, http_body = \ + http_status, http_reason_phrase, _http_headers, _http_body = \ self._request('/kra/rest/agent/keyrequests/' + request_id + '/cancel', self.kra_agent_port, self.POST, @@ -901,7 +901,7 @@ class kra: wrapped_passphrase) # Call CMS - http_status, http_reason_phrase, http_headers, http_body = \ + http_status, http_reason_phrase, _http_headers, http_body = \ self._request('/kra/rest/agent/keys/retrieve', self.kra_agent_port, self.POST, |