diff options
Diffstat (limited to 'base/kra/functional')
5 files changed, 2055 insertions, 0 deletions
diff --git a/base/kra/functional/drmclient.py b/base/kra/functional/drmclient.py new file mode 100644 index 000000000..e9b0ccb49 --- /dev/null +++ b/base/kra/functional/drmclient.py @@ -0,0 +1,1014 @@ +# Authors: +# Ade Lee <alee@redhat.com> +# +# Copyright (C) 2012 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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, 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +''' + +============================================================ +Python Test client for KRA using the new RESTful interface +============================================================ + +This is a python client that can be used to retrieve key requests +and keys from a KRA using the new RESTful interface. Moreover, given +a PKIArchiveOptions structure containing either a passphrase or a symmetric +key, this data can be stored in and retrieved from the KRA. + +A sample test execution is provided at the end of the file. +''' + +from lxml import etree +import nss.nss as nss +import httplib +from ipapython import nsslib, ipautil +from nss.error import NSPRError +from ipalib.errors import NetworkError, CertificateOperationError +from urllib import urlencode, quote_plus +from datetime import datetime +import logging +import base64 + +CERT_HEADER = "-----BEGIN NEW CERTIFICATE REQUEST-----" +CERT_FOOTER = "-----END NEW CERTIFICATE REQUEST-----" + +def _(string): + return string + +def parse_key_request_info_xml(doc): + ''' + :param doc: The root node of the xml document to parse + :returns: result dict + :except ValueError: + + After parsing the results are returned in a result dict. The following + table illustrates the mapping from the CMS data item to what may be found in + the result dict. If a CMS data item is absent it will also be absent in the + result dict. + + +----------------------+----------------+-----------------------+---------------+ + |cms name |cms type |result name |result type | + +======================+================+=======================+===============+ + |requestType |string |request_type |string | + +----------------------+----------------+-----------------------+---------------+ + |requestStatus |string |request_status |string | + +----------------------+----------------+-----------------------+---------------+ + |requestURL |string |request_id |string | + +----------------------+----------------+-----------------------+---------------+ + |keyURL |string |key_id |string | + +----------------------+----------------+-----------------------+---------------+ + ''' + response = {} + + request_type = doc.xpath('requestType') + if len(request_type) == 1: + request_type = etree.tostring(request_type[0], method='text', + encoding=unicode).strip() + response['request_type'] = request_type + + request_status = doc.xpath('requestStatus') + if len(request_status) == 1: + request_status = etree.tostring(request_status[0], method='text', + encoding=unicode).strip() + response['request_status'] = request_status + + request_url = doc.xpath('requestURL') + if len(request_url) == 1: + request_url = etree.tostring(request_url[0], method='text', + encoding=unicode).strip() + response['request_id'] = request_url.rsplit('/',1)[1] + + key_url = doc.xpath('keyURL') + if len(key_url) == 1: + key_url = etree.tostring(key_url[0], method='text', + encoding=unicode).strip() + response['key_id'] = key_url.rsplit('/',1)[1] + + return response + +def parse_key_request_infos_xml(doc): + ''' + :param doc: The root node of the xml document to parse + :returns: result dict + :except ValueError: + + After parsing the results are returned in a result dict. The following + table illustrates the mapping from the CMS data item to what may be found in + the result dict. If a CMS data item is absent it will also be absent in the + result dict. + + +----------------------+------------------------+-----------------------+---------------+ + |cms name |cms type |result name |result type | + +======================+========================+=======================+===============+ + |next |Link |next_id |unicode [1] | + +----------------------+------------------------+-----------------------+---------------+ + |prev |Link |prev_id |unicode [1] | + +----------------------+------------------------+-----------------------+---------------+ + |info for each request |SecurityDataRequestInfo |request_id [2] |dict | + +----------------------+------------------------+-----------------------+---------------+ + + [1] prev_id and next_id are the starting ids for the previous and next pages + respectively. They are extracted from the href elements of the Link + nodes (if they exist) + [2] For each key request info returned, we store a dict containing the key request data. + See parse_key_request_info_xml for details. Each dict is referenced by the id + of the key request (extracted from the key request URL). + ''' + response = {} + next_link = doc.xpath('//Link[@rel="next"]/href') + if len(next_link) == 1: + next_link = etree.tostring(next_link[0], method='text', + encoding=unicode).strip() + next_link = next_link.rsplit('/',1)[1] + response['next_id'] = next_link + + prev_link = doc.xpath('//Link[@rel="previous"]/href') + if len(prev_link) == 1: + prev_link = etree.tostring(prev_link[0], method='text', + encoding=unicode).strip() + prev_link = prev_link.rsplit('/', 1)[1] + response['prev_id'] = prev_link + + key_request_infos = doc.xpath('//SecurityDataRequestInfo') + for key_request in key_request_infos: + node = parse_key_request_info_xml(key_request) + response[node['request_id']] = node + + return response + +def parse_key_data_info_xml(doc): + ''' + :param doc: The root node of the xml document to parse + :returns: result dict + :except ValueError: + + After parsing the results are returned in a result dict. The following + table illustrates the mapping from the CMS data item to what may be found in + the result dict. If a CMS data item is absent it will also be absent in the + result dict. + + +----------------------+----------------+-----------------------+---------------+ + |cms name |cms type |result name |result type | + +======================+================+=======================+===============+ + |clientID |string |client_id |string | + +----------------------+----------------+-----------------------+---------------+ + |keyURL |string |key_url |string | + +----------------------+----------------+-----------------------+---------------+ + ''' + response = {} + + client_id = doc.xpath('clientID') + if len(client_id) == 1: + client_id = etree.tostring(client_id[0], method='text', + encoding=unicode).strip() + response['client_id'] = client_id + + key_url = doc.xpath('keyURL') + if len(key_url) == 1: + key_url = etree.tostring(key_url[0], method='text', + encoding=unicode).strip() + response['key_url'] = key_url + + return response + +def parse_key_data_infos_xml(doc): + ''' + :param doc: The root node of the xml document to parse + :returns: result dict + :except ValueError: + + After parsing the results are returned in a result dict. The following + table illustrates the mapping from the CMS data item to what may be found in + the result dict. If a CMS data item is absent it will also be absent in the + result dict. + + +----------------------+-----------------+-----------------------+---------------+ + |cms name |cms type |result name |result type | + +======================+=================+=======================+===============+ + |next |Link |next_id |unicode [1] | + +----------------------+-----------------+-----------------------+---------------+ + |prev |Link |prev_id |unicode [1] | + +----------------------+-----------------+-----------------------+---------------+ + |info for each key |SecurityDataInfo |key_id [2] |dict | + +----------------------+-----------------+-----------------------+---------------+ + + [1] prev_id and next_id are the starting ids for the previous and next pages + respectively. They are extracted from the href elements of the Link + nodes (if they exist) + [2] For each key info returned, we store a dict containing the key data. + See parse_key_data_info_xml for details. Each dict is referenced by the id + of the key (extracted from the key URL). + ''' + response = {} + + next_link = doc.xpath('//Link[@rel="next"]/href') + if len(next_link) == 1: + next_link = etree.tostring(next_link[0], method='text', + encoding=unicode).strip() + next_link = next_link.rsplit('/',1)[1] + response['next_id'] = next_link + + prev_link = doc.xpath('//Link[@rel="previous"]/href') + if len(prev_link) == 1: + prev_link = etree.tostring(prev_link[0], method='text', + encoding=unicode).strip() + prev_link = prev_link.rsplit('/', 1)[1] + response['prev_id'] = prev_link + + key_data_infos = doc.xpath('//SecurityDataInfo') + for key_data in key_data_infos: + node = parse_key_data_info_xml(key_data) + response[node['key_url'].rsplit('/',1)[1]] = node + + return response + +def parse_key_data_xml(doc): + ''' + :param doc: The root node of the xml document to parse + :returns: result dict + :except ValueError: + + After parsing the results are returned in a result dict. + + +----------------------+----------------+-----------------------+---------------+ + |cms name |cms type |result name |result type | + +======================+================+=======================+===============+ + |wrappedPrivateData |string |wrapped_data |unicode | + +----------------------+----------------+-----------------------+---------------+ + |nonceData |string |nonce_data |unicode | + +----------------------+----------------+-----------------------+---------------+ + + ''' + response = {} + + wrapped_data = doc.xpath('wrappedPrivateData') + if len(wrapped_data) == 1: + wrapped_data = etree.tostring(wrapped_data[0], method='text', + encoding=unicode).strip() + response['wrapped_data'] = wrapped_data + + nonce_data = doc.xpath('nonceData') + if len(nonce_data) == 1: + nonce_data = etree.tostring(nonce_data[0], method='text', + encoding=unicode).strip() + response['nonce_data'] = nonce_data + + return response + +def parse_certificate_data_xml(doc): + ''' + :param doc: The root node of the xml document to parse + :returns: result dict + :except ValueError: + + After parsing the results are returned in a result dict. + + +----------------------+----------------+-----------------------+---------------+ + |cms name |cms type |result name |result type | + +======================+================+=======================+===============+ + |b64 |string [1] |cert |unicode | + +----------------------+----------------+-----------------------+---------------+ + + [1] Base-64 encoded certificate with header and footer + ''' + response = {} + + b64 = doc.xpath('b64') + if len(b64) == 1: + b64 = etree.tostring(b64[0], method='text', + encoding=unicode).strip() + response['cert'] = b64.replace(CERT_HEADER, "").replace(CERT_FOOTER, "") + + return response + +def https_request(host, port, url, secdir, password, nickname, operation, args, **kw): + """ + :param url: The URL to post to. + :param operation: GET, POST, (PUT and DELETE not yet implemented) + :param args: arguments for GET command line, or for POST + :param kw: Keyword arguments to encode into POST body. + :return: (http_status, http_reason_phrase, http_headers, http_body) + as (integer, unicode, dict, str) + + Perform a client authenticated HTTPS request + """ + if isinstance(host, unicode): + host = host.encode('utf-8') + uri = 'https://%s%s' % (ipautil.format_netloc(host, port), url) + logging.info('sslget %r', uri) + + request_headers = {"Content-type": "application/xml", + "Accept": "application/xml"} + if operation == "POST": + if args != None: + post = args + elif kw != None: + post = urlencode(kw) + request_headers = {"Content-type": "application/x-www-form-urlencoded", + "Accept": "text/plain"} + try: + conn = nsslib.NSSConnection(host, port, dbdir=secdir) + conn.set_debuglevel(0) + conn.connect() + conn.sock.set_client_auth_data_callback(nsslib.client_auth_data_callback, + nickname, + password, nss.get_default_certdb()) + if operation == "GET": + url = url + "?" + args + conn.request("GET", url) + elif operation == "POST": + conn.request("POST", url, post, request_headers) + + res = conn.getresponse() + + http_status = res.status + http_reason_phrase = unicode(res.reason, 'utf-8') + http_headers = res.msg.dict + http_body = res.read() + conn.close() + except Exception, e: + raise NetworkError(uri=uri, error=str(e)) + + return http_status, http_reason_phrase, http_headers, http_body + +def http_request(host, port, url, operation, args): + """ + :param url: The URL to post to. + :param operation: GET, POST, (PUT and DELETE not yet implemented) + :param args: arguments for GET command line, or for POST + :return: (http_status, http_reason_phrase, http_headers, http_body) + as (integer, unicode, dict, str) + + Perform an HTTP request. + """ + if isinstance(host, unicode): + host = host.encode('utf-8') + uri = 'http://%s%s' % (ipautil.format_netloc(host, port), url) + logging.info('request %r', uri) + request_headers = {"Content-type": "application/xml", + "Accept": "application/xml"} + if operation == "POST": + if args != None: + post = args + else: + post = "" + conn = httplib.HTTPConnection(host, port) + try: + if operation == "GET": + if args != None: + url = url + "?" + args + conn.request("GET", url) + elif operation == "POST": + conn.request("POST", url, post, request_headers) + + res = conn.getresponse() + + http_status = res.status + http_reason_phrase = unicode(res.reason, 'utf-8') + http_headers = res.msg.dict + http_body = res.read() + conn.close() + except NSPRError, e: + raise NetworkError(uri=uri, error=str(e)) + + logging.debug('request status %d', http_status) + logging.debug('request reason_phrase %r', http_reason_phrase) + logging.debug('request headers %s', http_headers) + logging.debug('request body %r', http_body) + + return http_status, http_reason_phrase, http_headers, http_body + +class kra: + """ + Key Repository Authority backend plugin. + """ + + POST = "POST" + GET = "GET" + transport_cert = "byte array with transport cert" + mechanism = nss.CKM_DES_CBC_PAD + iv = "e4:bb:3b:d3:c3:71:2e:58" + fullname = "kra" + + + def __init__(self, work_dir, kra_host, kra_port, kra_nickname): + #crypto + self.sec_dir = work_dir + self.pwd_file = work_dir + "/pwdfile.txt" + self.transport_cert_nickname = kra_nickname + self.mechanism = nss.CKM_DES3_CBC_PAD + try: + f = open(self.pwd_file, "r") + self.password = f.readline().strip() + f.close() + except IOError: + self.password = '' + + #set up key db for crypto functions + try: + nss.nss_init(self.sec_dir) + except Exception, e: + raise CertificateOperationError(error=_('Error in initializing certdb (%s)') \ + + e.strerror) + self.transport_cert = nss.find_cert_from_nickname(self.transport_cert_nickname) + + # DRM info + self.kra_host = kra_host + self.kra_agent_port = kra_port + '''super(kra, self).__init__()''' + + def setup_contexts(self, mechanism, sym_key, iv): + # 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: + iv_data = nss.read_hex(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 debug(self, message, *args): + print message % args + + def _request(self, url, port, operation, args): + """ + :param url: The URL to post to. + :param port: The port to post to + :param operation: GET/POST/PUT/DELETE (as supported by sslget) + :param args: A string containing arguments for a GET or POST request + :return: (http_status, http_reason_phrase, http_headers, http_body) + as (integer, unicode, dict, str) + + Perform an HTTP request. + """ + return http_request(self.kra_host, port, url, operation, args) + + def _sslget(self, url, port, operation, args, **kw): + """ + :param url: The URL to post to. + :param port: The port to post to + :param operation: GET/POST/PUT/DELETE (as supported by sslget) + :param args: A string containing arguments for a GET or POST request + :param kw: Alternatively, keyword arguments to be form-encoded into POST body. + :return: (http_status, http_reason_phrase, http_headers, http_body) + as (integer, unicode, dict, str) + + Perform an HTTPS request + """ + return https_request(self.kra_host, port, url, self.sec_dir, self.password, + self.ipa_certificate_nickname, operation, args, **kw) + + def symmetric_wrap(self, data, wrapping_key): + """ + :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(self.mechanism, wrapping_key, self.iv) + wrapped_data = encoding_ctx.cipher_op(data) + encoding_ctx.digest_final() + return wrapped_data + + def asymmetric_wrap(self, data, wrapping_cert): + """ + :param data: Data to be wrapped + :param wrapping_cert Public key to wrap data + + Wrap (encrypt) data using the supplied asymmetric key + """ + + return None + + def symmetric_unwrap(self, data, wrapping_key, iv = None): + """ + :param data: Data to be unwrapped + :param wrapping_key Symmetric key to unwrap data + + Unwrap (decrypt) data using the supplied symmetric key + """ + if iv == None: + iv = self.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 + + def get_parse_result_xml(self, xml_text, parse_func): + ''' + :param xml_text: The XML text to parse + :param parse_func: The XML parsing function to apply to the parsed DOM tree. + :return: parsed result dict + + Utility routine which parses the input text into an XML DOM tree + and then invokes the parsing function on the DOM tree in order + to get the parsing result as a dict of key/value pairs. + ''' + parser = etree.XMLParser() + doc = etree.fromstring(xml_text, parser) + result = parse_func(doc) + self.debug("%s() xml_text:\n%s\nparse_result:\n%s" % (parse_func.__name__, xml_text, result)) + return result + + def create_archival_request(self, client_id, security_data, data_type): + """ + :param :param client_id: identifier to be used for this stored key + :param security_data: data blob (PKIArchiveOptions) containing passphrase + or symmetric key to be archived + :param data_type: data type (symmetricKey, pass_phrase, asymmetricKey) + :return doc: xml doc with archival request + """ + self.debug('%s.create_archival_request()', self.fullname) + root = etree.Element("SecurityDataArchivalRequest") + client_id_element = etree.SubElement(root, "clientId") + client_id_element.text = client_id + wrapped_private_data_element = etree.SubElement(root, "wrappedPrivateData") + wrapped_private_data_element.text = security_data + data_type_element = etree.SubElement(root, "dataType") + data_type_element.text = data_type + return etree.ElementTree(root) + + def create_recovery_request(self, key_id, request_id, session_key, passphrase, nonce = None): + """ + :param key_id: identifier of key to be recovered + :param request_id: id for the recovery request + :param session_key session key wrapped in transport key + :param passphrase passphrase wrapped in session key + :return doc: xml doc with archival request + + """ + self.debug('%s.create_recovery_request()', self.fullname) + root = etree.Element("SecurityDataRecoveryRequest") + if key_id != None: + key_id_element = etree.SubElement(root, "keyId") + key_id_element.text = key_id + if request_id != None: + request_id_element = etree.SubElement(root, "requestId") + request_id_element.text = request_id + if session_key != None: + session_key_element = etree.SubElement(root, "transWrappedSessionKey") + session_key_element.text = session_key + if passphrase != None: + passphrase_element = etree.SubElement(root, "sessionWrappedPassphrase") + passphrase_element.text = passphrase + if nonce != None: + nonce_element = etree.SubElement(root, "nonceData") + nonce_element.text = nonce + return etree.ElementTree(root) + + def archive_security_data(self, client_id, security_data, data_type): + """ + :param client_id: identifier to be used for this stored key + :param security_data: data blob (PKIArchiveOptions) containing passphrase + or symmetric key to be archived + :param data_type: data type (symmetricKey, pass_phrase, asymmetricKey) + + Archives security data packaged in a PKIArchiveOptions blob + + The command returns a dict with key/value pairs as defined in + parse_key_request_info_xml(). These include the request_id of the created + archival request, the status of the request, and the key_id of the archived + key. + """ + self.debug('%s.archive_security_data()', self.fullname) + + # check clientID and security data + if ((client_id == None) or (security_data == None)): + raise CertificateOperationError(error=_('Bad arguments to archive_security_data')) + + request = self.create_archival_request(client_id, security_data, data_type) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/keyrequest/archive', + self.kra_agent_port, + self.POST, + etree.tostring(request.getroot(), encoding='UTF-8')) + + # Parse and handle errors + if (http_status != 200): + raise CertificateOperationError(error=_('Error in archiving request (%s)') % \ + http_reason_phrase) + + parse_result = self.get_parse_result_xml(http_body, parse_key_request_info_xml) + return parse_result + + def get_transport_cert(self, etag=None): + """ + :param etag: etag info for last cert retrieval from DRM + + Gets the transport certificate from the DRM + + The command returns a dict as defined in parse_certificate_data_xml() + """ + self.debug('%s.get_transport_cert()', self.fullname) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/config/cert/transport', + self.kra_agent_port, + self.GET, + None) + + self.debug("headers: %s" , http_headers) + # Parse and handle errors + if (http_status != 200): + raise CertificateOperationError(error=_('Error in archiving request (%s)') % \ + http_reason_phrase) + + parse_result = self.get_parse_result_xml(http_body, parse_certificate_data_xml) + return parse_result + + def list_security_data(self, client_id, key_state = None, next_id = None): + """ + :param client_id: identifier to be searched for + :param key_state: state for key (active, inactive, all) + :param next_id: id for starting key on next page (if more than one page) + + List security data matching the specified client id and state + + The command returns a dict as specified in parse_key_data_infos_xml(). + """ + self.debug('%s.list_security_data()', self.fullname) + if client_id == None: + raise CertificateOperationError(error=_('Bad argument to list_security_data')) + get_args = "clientID=" + quote_plus(client_id) + + if key_state != None: + get_args = get_args + "&status=" + quote_plus(key_state) + + if next_id != None: + # currnently not implemented on server + get_args = get_args + "&start=" + quote_plus(next_id) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/keys', + self.kra_agent_port, + self.GET, + get_args) + + # Parse and handle errors + if (http_status != 200): + raise CertificateOperationError(error=_('Error in listing keys (%s)') % \ + http_reason_phrase) + + parse_result = self.get_parse_result_xml(http_body, parse_key_data_infos_xml) + return parse_result + + def list_key_requests(self, request_state = None, request_type = None, client_id = None, + next_id = None): + """ + :param request_state: state of request (pending, complete, cancelled, rejected, approved) + :param request_type: request type (enrollment, recovery) + :param next_id: id for starting key on next page (if more than one page) + + List security data matching the specified client id and state + + The command returns a dict as specified in parse_key_request_infos_xml(). + """ + self.debug('%s.list_key_requests()', self.fullname) + get_args = "" + + if request_state != None: + get_args = get_args + "&requestState=" + quote_plus(request_state) + + if request_type != None: + get_args = get_args + "&requestType=" + quote_plus(request_type) + + if client_id != None: + get_args = get_args + "&clientID=" + quote_plus(client_id) + + if next_id != None: + # currnently not implemented on server + get_args = get_args + "&start=" + quote_plus(next_id) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/keyrequests', + self.kra_agent_port, + self.GET, + get_args) + + # Parse and handle errors + if (http_status != 200): + raise CertificateOperationError(error=_('Error in listing key requests (%s)') % \ + http_reason_phrase) + + parse_result = self.get_parse_result_xml(http_body, parse_key_request_infos_xml) + return parse_result + + def submit_recovery_request(self, key_id): + """ + :param key_id: identifier of data to be recovered + + Create a recovery request for a passphrase or symmetric key + + The command returns a dict as described in the comments to + parse_key_request_info_xml(). This data includes the request_id + of the created recovery request + """ + self.debug('%s.submit_recovery_request()', self.fullname) + + # check clientID and security data + if key_id == None: + raise CertificateOperationError(error=_('Bad argument to archive_security_data')) + + request = self.create_recovery_request(key_id, None, None, None) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/keyrequest/recover', + self.kra_agent_port, + self.POST, + etree.tostring(request.getroot(), encoding='UTF-8')) + + # Parse and handle errors + if (http_status != 200): + raise CertificateOperationError(error=_('Error in archiving request (%s)') % \ + http_reason_phrase) + + parse_result = self.get_parse_result_xml(http_body, parse_key_request_info_xml) + return parse_result + + def check_request_status(self, request_id): + """ + :param recovery_request_id: identifier of key recovery request + + Check recovery request status + + The command returns a dict with these possible key/value pairs. + Some key/value pairs may be absent + + +-----------------+---------------+-------------------------------------- + + |result name |result type |comments | + +=================+===============+=======================================+ + |request_status |String | status of request (pending, rejected, | + | | | approved) | + +-----------------+---------------+---------------------------------------| + |approvers_needed |int | If pending, number of approvers | + | | | needed | + +-----------------+---------------+---------------------------------------+ + |approvers_list |String | list of approvers | + +-----------------+---------------+---------------------------------------+ + """ + self.debug('%s.check_request_status()', self.fullname) + + def approve_recovery_request(self, request_id): + """ + :param request_id: identifier of key recovery request + + Approve recovery request + """ + self.debug('%s.approve_recovery_request()', self.fullname) + if request_id == None: + raise CertificateOperationError(error=_('Bad argument to approve_recovery_request')) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/keyrequest/approve/'+ request_id, + self.kra_agent_port, + self.POST, + None) + + # Parse and handle errors + if (http_status > 399): + raise CertificateOperationError(error=_('Error in approving request (%s)') % \ + http_reason_phrase) + + def reject_recovery_request(self, request_id): + """ + :param recovery_request_id: identifier of key recovery request + + Reject recovery request + """ + self.debug('%s.reject_recovery_request()', self.fullname) + if request_id == None: + raise CertificateOperationError(error=_('Bad argument to reject_recovery_request')) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/keyrequest/reject/'+ request_id, + self.kra_agent_port, + self.POST, + None) + + # Parse and handle errors + if (http_status > 399): + raise CertificateOperationError(error=_('Error in rejecting request (%s)') % \ + http_reason_phrase) + + def cancel_recovery_request(self, request_id): + """ + :param recovery_request_id: identifier of key recovery request + + Cancel recovery request + """ + self.debug('%s.cancel_recovery_request()', self.fullname) + if request_id == None: + raise CertificateOperationError(error=_('Bad argument to cancel_recovery_request')) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/keyrequest/cancel/'+ request_id, + self.kra_agent_port, + self.POST, + None) + + # Parse and handle errors + if (http_status > 399): + raise CertificateOperationError(error=_('Error in cancelling request (%s)') % \ + http_reason_phrase) + + def retrieve_security_data(self, recovery_request_id, passphrase=None): + """ + :param recovery_request_id: identifier of key recovery request + :param passphrase: passphrase to be used to wrap the data + + Recover the passphrase or symmetric key. We require an approved + recovery request. + + If a passphrase is provided, the DRM will return a blob that can be decrypted + with the passphrase. If not, then a symmetric key will be created to wrap the + data for transport to this server. Upon receipt, the data will be unwrapped + and returned unencrypted. + + The command returns a dict with the values described in parse_key_data_xml(), + as well as the following field + + +-----------------+---------------+-------------------------------------- + + |result name |result type |comments | + +=================+===============+=======================================+ + |data |String | Key data (either wrapped using | + | | | passphrase or unwrapped) | + +-----------------+---------------+---------------------------------------+ + """ + self.debug('%s.retrieve_security_data()', self.fullname) + + if recovery_request_id == None: + raise CertificateOperationError(error=_('Bad arguments to retrieve_security_data')) + + # generate symmetric key + slot = nss.get_best_slot(self.mechanism) + session_key = slot.key_gen(self.mechanism, None, slot.get_best_key_length(self.mechanism)) + + # wrap this key with the transport cert + public_key = self.transport_cert.subject_public_key_info.public_key + wrapped_session_key = base64.b64encode(nss.pub_wrap_sym_key(self.mechanism, public_key, session_key)) + wrapped_passphrase = None + if passphrase != None: + # wrap passphrase with session key + wrapped_session_key = base64.b64encode(self.symmetric_wrap(passphrase, session_key)) + + request = self.create_recovery_request(None, recovery_request_id, + wrapped_session_key, + wrapped_passphrase) + + #Call CMS + http_status, http_reason_phrase, http_headers, http_body = \ + self._request('/kra/pki/key/retrieve', + self.kra_agent_port, + self.POST, + etree.tostring(request.getroot(), encoding='UTF-8')) + + # Parse and handle errors + if (http_status != 200): + raise CertificateOperationError(error=_('Error in retrieving security data (%s)') % \ + http_reason_phrase) + + parse_result = self.get_parse_result_xml(http_body, parse_key_data_xml) + + if passphrase == None: + iv = nss.data_to_hex(base64.decodestring(parse_result['nonce_data'])) + parse_result['data'] = self.symmetric_unwrap(base64.decodestring(parse_result['wrapped_data']), + session_key, iv) + + return parse_result + + def recover_security_data(self, key_id, passphrase=None): + """ + :param key_id: identifier of key to be recovered + :param passphrase: passphrase to wrap key data for delivery outside of this server + + Recover the key data (symmetric key or passphrase) in a one step process. + This is the case when only one approver is required to extract a key such that + the agent submitting the recovery request is the only approver required. + + In this case, the request is automatically approved, and the KRA just returns the + key data. + + This has not yet been implemented on the server + """ + self.debug('%s.recover_security_data()', self.fullname) + pass + +""" Sample Test execution starts here """ +import argparse + +parser = argparse.ArgumentParser(description="Sample Test execution") +parser.add_argument('-d', default='/tmp/drmtest', dest='work_dir', help='Working directory') +parser.add_argument('--options', default='options.out', dest='options_file', + help='File containing test PKIArchiveOptions to be archived') +parser.add_argument('--symkey', default='symkey.out', dest='symkey_file', + help='File containing test symkey') +parser.add_argument('--host', default='localhost', dest='kra_host', help='DRM hostname') +parser.add_argument('-p', default='10080', type=int, dest='kra_port', help='DRM Port') +parser.add_argument('-n', default='DRM TransportCert Nickname', dest='kra_nickname', + help="DRM Nickname") + +args = parser.parse_args() +work_dir = args.work_dir +kra_host = args.kra_host +kra_port = args.kra_port +kra_nickname = args.kra_nickname +options_file = args.options_file +symkey_file = args.symkey_file + +test_kra = kra(work_dir, kra_host, kra_port, kra_nickname) + +# list requests +requests = test_kra.list_key_requests() +print requests + +# get transport cert +transport_cert = test_kra.get_transport_cert() +print transport_cert + +#archive symmetric key +f = open(work_dir + "/" + options_file) +wrapped_key = f.read() +client_id = "Python symmetric key " + datetime.now().strftime("%Y-%m-%d %H:%M") +response = test_kra.archive_security_data(client_id, wrapped_key,"symmetricKey") +print response + +# list keys with client_id +response = test_kra.list_security_data(client_id, "active") +print response + +#create recovery request +key_id = response.keys()[0] +print key_id +response = test_kra.submit_recovery_request(key_id) +print response + +# approve recovery request +request_id = response['request_id'] +test_kra.approve_recovery_request(request_id) + +# test invalid request +print "Testing invalid request ID" +try: + response = test_kra.retrieve_security_data("INVALID") + print "Failure: No exception thrown" +except CertificateOperationError, e: + if 'Error in retrieving security data (Bad Request)' == e.error: + print "Success: " + e.error + else: + print "Failure: Wrong error message: " + e.error + +# retrieve key +response = test_kra.retrieve_security_data(request_id) +print response +print "retrieved data is " + base64.encodestring(response['data']) + +#read original symkey from file +f = open(work_dir + "/" + symkey_file) +orig_key = f.read() +print "orig key is " + orig_key + +if orig_key.strip() == base64.encodestring(response['data']).strip(): + print "Success: the keys match" +else: + print "Failure: keys do not match" diff --git a/base/kra/functional/drmclient.readme.txt b/base/kra/functional/drmclient.readme.txt new file mode 100644 index 000000000..833c5ce3c --- /dev/null +++ b/base/kra/functional/drmclient.readme.txt @@ -0,0 +1,50 @@ +Running drmclient.py: + +The python drmclient currently requires a little setup to be run. + +1. Create a working directory - the code uses /tmp/drmtest +2. In that directory, create an NSS database. In this doc, we will use the + password redhat123 as the password for the NSS db. + + certutil -N -d /tmp/drmtest + +3. Add a password file /tmp/drmtest/pwdfile.txt. It should contain the password for + the NSS database. + +4. Put the transport certificate in a file /tmp/drmtest/transport.crt in binary format. + + certutil -L -d /var/lib/pki-kra/alias -n "DRM Transport Certificate" -a > /tmp/drmtest/transport.asc + AtoB /tmp/drmtest/transport.asc /tmp/drmtest/transport.crt + +5. Import the transport certificate into the certificate databse in /tmp/drmtest. + certutil -A -d /tmp/drmtest -n "DRM Transport Certificate" -i /tmp/drmtest/transport.asc + +5. Run GeneratePKIArchiveOptions to generate some test data. Specifically we will be + using it to generate a symmetric key and its associated PKIArchoveOptions structure + to be archived. + + GeneratePKIArchiveOptions -k /tmp/drmtest/symkey.out -w redhat123 -t /tmp/drmtest -o /tmp/drmtest/options.out + +6. Run the python code. You will likely need some python modules - python-lxml, python-nss + and ipapython. + + The code has the following usage: + +usage: drmclient.py [-h] [-d WORK_DIR] [--options OPTIONS_FILE] + [--symkey SYMKEY_FILE] [--host KRA_HOST] [-p KRA_PORT] + [-n KRA_NICKNAME] + +Sample Test execution + +optional arguments: + -h, --help show this help message and exit + -d WORK_DIR Working directory + --options OPTIONS_FILE + File containing test PKIArchiveOptions to be archived + --symkey SYMKEY_FILE File containing test symkey + --host KRA_HOST DRM hostname + -p KRA_PORT DRM Port + -n KRA_NICKNAME DRM Nickname + +For example: +python pki/base/kra/functional/drmclient.py -d /tmp/drmtest -p 10200 -n "DRM Transport Certificate - alee eclipse domain 2" diff --git a/base/kra/functional/src/com/netscape/cms/servlet/test/DRMRestClient.java b/base/kra/functional/src/com/netscape/cms/servlet/test/DRMRestClient.java new file mode 100644 index 000000000..651873b20 --- /dev/null +++ b/base/kra/functional/src/com/netscape/cms/servlet/test/DRMRestClient.java @@ -0,0 +1,266 @@ +package com.netscape.cms.servlet.test; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; + +import org.apache.commons.httpclient.ConnectTimeoutException; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.params.HttpConnectionParams; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.jboss.resteasy.client.ClientExecutor; +import org.jboss.resteasy.client.ClientResponse; +import org.jboss.resteasy.client.ProxyFactory; +import com.netscape.certsrv.dbs.keydb.KeyId; +import com.netscape.certsrv.request.RequestId; +import org.jboss.resteasy.client.core.executors.ApacheHttpClientExecutor; +import com.netscape.cms.servlet.admin.SystemCertificateResource; +import com.netscape.cms.servlet.cert.model.CertificateData; +import com.netscape.cms.servlet.key.KeyResource; +import com.netscape.cms.servlet.key.KeysResource; +import com.netscape.cms.servlet.key.model.KeyData; +import com.netscape.cms.servlet.key.model.KeyDataInfo; +import com.netscape.cms.servlet.key.model.KeyDataInfos; +import com.netscape.cms.servlet.request.KeyRequestResource; +import com.netscape.cms.servlet.request.KeyRequestsResource; +import com.netscape.cms.servlet.request.model.ArchivalRequestData; +import com.netscape.cms.servlet.request.model.KeyRequestInfo; +import com.netscape.cms.servlet.request.model.KeyRequestInfos; +import com.netscape.cms.servlet.request.model.RecoveryRequestData; +import com.netscape.cmsutil.util.Utils; + +import org.mozilla.jss.ssl.SSLCertificateApprovalCallback; +import org.mozilla.jss.ssl.SSLClientCertificateSelectionCallback; +import org.mozilla.jss.ssl.SSLSocket; + +public class DRMRestClient { + + // Callback to approve or deny returned SSL server certs + // Right now, simply approve the cert. + // ToDO: Look into taking this JSS http client code and move it into + // its own class to be used by possible future clients. + private class ServerCertApprovalCB implements SSLCertificateApprovalCallback { + + public boolean approve(org.mozilla.jss.crypto.X509Certificate servercert, + SSLCertificateApprovalCallback.ValidityStatus status) { + + //For now lets just accept the server cert. This is a test tool, being + // pointed at a well know kra instance. + + + if (servercert != null) { + System.out.println("Peer cert details: " + + "\n subject: " + servercert.getSubjectDN().toString() + + "\n issuer: " + servercert.getIssuerDN().toString() + + "\n serial: " + servercert.getSerialNumber().toString() + ); + } + + SSLCertificateApprovalCallback.ValidityItem item; + + Enumeration<?> errors = status.getReasons(); + int i = 0; + while (errors.hasMoreElements()) { + i++; + item = (SSLCertificateApprovalCallback.ValidityItem) errors.nextElement(); + System.out.println("item " + i + + " reason=" + item.getReason() + + " depth=" + item.getDepth()); + + int reason = item.getReason(); + + if (reason == + SSLCertificateApprovalCallback.ValidityStatus.UNTRUSTED_ISSUER || + reason == SSLCertificateApprovalCallback.ValidityStatus.BAD_CERT_DOMAIN) { + + //Allow these two since we haven't necessarily installed the CA cert for trust + // and we are choosing "localhost" as the host for this client. + + return true; + + } + } + + //For other errors return false + + return false; + } + } + + private class JSSProtocolSocketFactory implements ProtocolSocketFactory { + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + + SSLSocket sock = createJSSSocket(host,port, null, 0, null); + return (Socket) sock; + } + + @Override + public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException, + UnknownHostException { + + SSLSocket sock = createJSSSocket(host,port, clientHost, clientPort, null); + return (Socket) sock; + } + + @Override + public Socket createSocket(String host, int port, InetAddress localAddress, int localPort, HttpConnectionParams params) + throws IOException, UnknownHostException, ConnectTimeoutException { + + SSLSocket sock = createJSSSocket(host, port, localAddress, localPort, null); + return (Socket) sock; + } + } + + private SSLSocket createJSSSocket(String host, int port, InetAddress localAddress, + int localPort, SSLClientCertificateSelectionCallback clientCertSelectionCallback) + throws IOException, UnknownHostException, ConnectTimeoutException { + + SSLSocket sock = new SSLSocket(InetAddress.getByName(host), + port, + localAddress, + localPort, + new ServerCertApprovalCB(), + null); + + if(sock != null && clientCertNickname != null) { + sock.setClientCertNickname(clientCertNickname); + } + + return sock; + + } + private KeyResource keyClient; + private KeysResource keysClient; + private KeyRequestsResource keyRequestsClient; + private KeyRequestResource keyRequestClient; + private SystemCertificateResource systemCertClient; + + private String clientCertNickname = null; + + public DRMRestClient(String baseUri, String clientCertNick) throws MalformedURLException { + + // For SSL we are assuming the caller has already intialized JSS and has + // a valid CryptoManager and CryptoToken + // optional clientCertNickname is provided for use if required. + + + URL url = new URL(baseUri); + + String protocol = url.getProtocol(); + int port = url.getPort(); + + clientCertNickname = null; + if(protocol != null && protocol.equals("https")) { + if (clientCertNick != null) { + clientCertNickname = clientCertNick; + } + + Protocol.registerProtocol("https", + new Protocol(protocol, new JSSProtocolSocketFactory(), port)); + } + + HttpClient httpclient = new HttpClient(); + ClientExecutor executor = new ApacheHttpClientExecutor(httpclient); + + systemCertClient = ProxyFactory.create(SystemCertificateResource.class, baseUri, executor); + keyRequestsClient = ProxyFactory.create(KeyRequestsResource.class, baseUri, executor); + keyRequestClient = ProxyFactory.create(KeyRequestResource.class, baseUri, executor); + keysClient = ProxyFactory.create(KeysResource.class, baseUri, executor); + keyClient = ProxyFactory.create(KeyResource.class, baseUri, executor); + } + + public String getTransportCert() { + @SuppressWarnings("unchecked") + ClientResponse<CertificateData> response = (ClientResponse<CertificateData>) systemCertClient.getTransportCert(); + CertificateData certData = response.getEntity(); + String transportCert = certData.getB64(); + return transportCert; + } + + public Collection<KeyRequestInfo> listRequests(String requestState, String requestType) { + KeyRequestInfos infos = keyRequestsClient.listRequests( + requestState, requestType, null, new RequestId(0), 100, 100, 10 + ); + Collection<KeyRequestInfo> list = infos.getRequests(); + return list; + } + + public KeyRequestInfo archiveSecurityData(byte[] encoded, String clientId, String dataType) { + // create archival request + ArchivalRequestData data = new ArchivalRequestData(); + String req1 = Utils.base64encode(encoded); + data.setWrappedPrivateData(req1); + data.setClientId(clientId); + data.setDataType(dataType); + + KeyRequestInfo info = keyRequestClient.archiveKey(data); + return info; + } + + public KeyDataInfo getKeyData(String clientId, String status) { + KeyDataInfos infos = keysClient.listKeys(clientId, status, 100, 10); + Collection<KeyDataInfo> list = infos.getKeyInfos(); + Iterator<KeyDataInfo> iter = list.iterator(); + + while (iter.hasNext()) { + KeyDataInfo info = iter.next(); + if (info != null) { + // return the first one + return info; + } + } + return null; + } + + public KeyRequestInfo requestRecovery(KeyId keyId, byte[] rpwd, byte[] rkey, byte[] nonceData) { + // create recovery request + RecoveryRequestData data = new RecoveryRequestData(); + data.setKeyId(keyId); + if (rpwd != null) { + data.setSessionWrappedPassphrase(Utils.base64encode(rpwd)); + } + if (rkey != null) { + data.setTransWrappedSessionKey(Utils.base64encode(rkey)); + } + + if (nonceData != null) { + data.setNonceData(Utils.base64encode(nonceData)); + } + + KeyRequestInfo info = keyRequestClient.recoverKey(data); + return info; + } + + public void approveRecovery(RequestId recoveryId) { + keyRequestClient.approveRequest(recoveryId); + } + + public KeyData retrieveKey(KeyId keyId, RequestId requestId, byte[] rpwd, byte[] rkey, byte[] nonceData) { + // create recovery request + RecoveryRequestData data = new RecoveryRequestData(); + data.setKeyId(keyId); + data.setRequestId(requestId); + if (rkey != null) { + data.setTransWrappedSessionKey(Utils.base64encode(rkey)); + } + if (rpwd != null) { + data.setSessionWrappedPassphrase(Utils.base64encode(rpwd)); + } + + if (nonceData != null) { + data.setNonceData(Utils.base64encode(nonceData)); + } + + KeyData key = keyClient.retrieveKey(data); + return key; + } +} diff --git a/base/kra/functional/src/com/netscape/cms/servlet/test/DRMTest.java b/base/kra/functional/src/com/netscape/cms/servlet/test/DRMTest.java new file mode 100644 index 000000000..8d83247b8 --- /dev/null +++ b/base/kra/functional/src/com/netscape/cms/servlet/test/DRMTest.java @@ -0,0 +1,503 @@ +// --- BEGIN COPYRIGHT BLOCK --- +// 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. +// +// (C) 2012 Red Hat, Inc. +// All rights reserved. +// --- END COPYRIGHT BLOCK --- +package com.netscape.cms.servlet.test; + +import java.net.MalformedURLException; +import java.util.Calendar; +import java.util.Collection; +import java.util.Iterator; +import java.util.Random; + +import org.mozilla.jss.CryptoManager; +import org.mozilla.jss.crypto.AlreadyInitializedException; +import org.mozilla.jss.crypto.CryptoToken; +import org.mozilla.jss.crypto.EncryptionAlgorithm; +import org.mozilla.jss.crypto.IVParameterSpec; +import org.mozilla.jss.crypto.KeyGenAlgorithm; +import org.mozilla.jss.crypto.SymmetricKey; +import org.mozilla.jss.util.Password; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.cli.PosixParser; + +import com.netscape.certsrv.dbs.keydb.KeyId; +import com.netscape.certsrv.request.RequestId; +import com.netscape.cms.servlet.base.CMSResourceService; +import com.netscape.cms.servlet.key.model.KeyData; +import com.netscape.cms.servlet.key.model.KeyDataInfo; +import com.netscape.cms.servlet.request.KeyRequestResource; +import com.netscape.cms.servlet.request.model.KeyRequestInfo; +import com.netscape.cmsutil.crypto.CryptoUtil; +import com.netscape.cmsutil.util.Utils; + +public class DRMTest { + + public static void usage(Options options) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("DRMTest", options); + System.exit(1); + } + + public static void main(String args[]) { + String host = null; + String port = null; + String token_pwd = null; + String db_dir = "./"; + String protocol = "http"; + String clientCertNickname = "KRA Administrator of Instance pki-kra's SjcRedhat Domain ID"; + + // parse command line arguments + Options options = new Options(); + options.addOption("h", true, "Hostname of the DRM"); + options.addOption("p", true, "Port of the DRM"); + options.addOption("w", true, "Token password"); + options.addOption("d", true, "Directory for tokendb"); + options.addOption("s", true, "Attempt Optional Secure SSL connection"); + options.addOption("c", true, "Optional SSL Client cert Nickname"); + + try { + CommandLineParser parser = new PosixParser(); + CommandLine cmd = parser.parse(options, args); + + if (cmd.hasOption("h")) { + host = cmd.getOptionValue("h"); + } else { + System.err.println("Error: no hostname provided."); + usage(options); + } + + if (cmd.hasOption("p")) { + port = cmd.getOptionValue("p"); + } else { + System.err.println("Error: no port provided"); + usage(options); + } + + if (cmd.hasOption("w")) { + token_pwd = cmd.getOptionValue("w"); + } else { + System.err.println("Error: no token password provided"); + usage(options); + } + + if (cmd.hasOption("d")) { + db_dir = cmd.getOptionValue("d"); + } + + if (cmd.hasOption("s")) { + if(cmd.getOptionValue("s") != null && cmd.getOptionValue("s").equals("true")) { + protocol = "https"; + } + } + + if (cmd.hasOption("c")) { + String nick = cmd.getOptionValue("c"); + + if (nick != null && protocol.equals("https")) { + clientCertNickname = nick; + } + } + + } catch (ParseException e) { + System.err.println("Error in parsing command line options: " + e.getMessage()); + usage(options); + } + + // used for crypto operations + byte iv[] = { 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1 }; + IVParameterSpec ivps = null; + IVParameterSpec ivps_server = null; + + try { + ivps = genIV(8); + } catch (Exception e) { + log("Can't generate initialization vector use default: " + e.toString()); + ivps = new IVParameterSpec(iv); + } + + CryptoManager manager = null; + CryptoToken token = null; + + // used for wrapping to send data to DRM + String transportCert = null; + + // Data to be archived + SymmetricKey vek = null; + String passphrase = null; + + // Session keys and passphrases for recovery + SymmetricKey recoveryKey = null; + byte[] wrappedRecoveryKey = null; + String recoveryPassphrase = null; + byte[] wrappedRecoveryPassphrase = null; + + // retrieved data (should match archived data) + String wrappedRecoveredKey = null; + String recoveredKey = null; + + // various ids used in recovery/archival operations + KeyId keyId = null; + String clientId = null; + RequestId recoveryRequestId = null; + + // Variables for data structures from calls + KeyRequestInfo requestInfo = null; + KeyData keyData = null; + KeyDataInfo keyInfo = null; + + // Initialize token + try { + CryptoManager.initialize(db_dir); + } catch (AlreadyInitializedException e) { + // it is ok if it is already initialized + } catch (Exception e) { + log("INITIALIZATION ERROR: " + e.toString()); + System.exit(1); + } + + // log into token + try { + manager = CryptoManager.getInstance(); + token = manager.getInternalKeyStorageToken(); + Password password = new Password(token_pwd.toCharArray()); + try { + token.login(password); + } catch (Exception e) { + log("login Exception: " + e.toString()); + if (!token.isLoggedIn()) { + token.initPassword(password, password); + } + } + } catch (Exception e) { + log("Exception in logging into token:" + e.toString()); + } + + // Set base URI and get client + + + String baseUri = protocol + "://" + host + ":" + port + "/kra/pki"; + DRMRestClient client; + try { + client = new DRMRestClient(baseUri, clientCertNickname); + } catch (MalformedURLException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + return; + } + + // Test 1: Get transport certificate from DRM + transportCert = client.getTransportCert(); + transportCert = transportCert.substring(CMSResourceService.HEADER.length(), + transportCert.indexOf(CMSResourceService.TRAILER)); + + log("Transport Cert retrieved from DRM: " + transportCert); + + // Test 2: Get list of completed key archival requests + log("\n\nList of completed archival requests"); + Collection<KeyRequestInfo> list = client.listRequests("complete", "securityDataEnrollment"); + if (list == null) { + log("No requests found"); + } else { + Iterator<KeyRequestInfo> iter = list.iterator(); + while (iter.hasNext()) { + KeyRequestInfo info = iter.next(); + printRequestInfo(info); + } + } + + // Test 3: Get list of key recovery requests + log("\n\nList of completed recovery requests"); + Collection<KeyRequestInfo> list2 = client.listRequests("complete", "securityDataRecovery"); + if (list2 == null) { + log("No requests found"); + } else { + Iterator<KeyRequestInfo> iter2 = list2.iterator(); + while (iter2.hasNext()) { + KeyRequestInfo info = iter2.next(); + printRequestInfo(info); + } + } + + // Test 4: Generate and archive a symmetric key + log("Archiving symmetric key"); + clientId = "UUID: 123-45-6789 VEK " + Calendar.getInstance().getTime().toString(); + try { + vek = CryptoUtil.generateKey(token, KeyGenAlgorithm.DES3); + byte[] encoded = CryptoUtil.createPKIArchiveOptions(manager, token, transportCert, vek, null, + KeyGenAlgorithm.DES3, ivps); + + KeyRequestInfo info = client.archiveSecurityData(encoded, clientId, KeyRequestResource.SYMMETRIC_KEY_TYPE); + log("Archival Results:"); + printRequestInfo(info); + keyId = info.getKeyId(); + } catch (Exception e) { + log("Exception in archiving symmetric key:" + e.getMessage()); + e.printStackTrace(); + } + + //Test 5: Get keyId for active key with client ID + + log("Getting key ID for symmetric key"); + keyInfo = client.getKeyData(clientId, "active"); + KeyId keyId2 = keyInfo.getKeyId(); + if (keyId2 == null) { + log("No archived key found"); + } else { + log("Archived Key found: " + keyId); + } + + if (!keyId.equals(keyId2)) { + log("Error: key ids from search and archival do not match"); + } else { + log("Success: keyids from search and archival match."); + } + + // Test 6: Submit a recovery request for the symmetric key using a session key + log("Submitting a recovery request for the symmetric key using session key"); + try { + recoveryKey = CryptoUtil.generateKey(token, KeyGenAlgorithm.DES3); + wrappedRecoveryKey = CryptoUtil.wrapSymmetricKey(manager, token, transportCert, recoveryKey); + KeyRequestInfo info = client.requestRecovery(keyId, null, wrappedRecoveryKey, ivps.getIV()); + recoveryRequestId = info.getRequestId(); + } catch (Exception e) { + log("Exception in recovering symmetric key using session key: " + e.getMessage()); + } + + // Test 7: Approve recovery + log("Approving recovery request: " + recoveryRequestId); + client.approveRecovery(recoveryRequestId); + + // Test 8: Get key + log("Getting key: " + keyId); + + keyData = client.retrieveKey(keyId, recoveryRequestId, null, wrappedRecoveryKey, ivps.getIV()); + wrappedRecoveredKey = keyData.getWrappedPrivateData(); + + ivps_server = new IVParameterSpec(Utils.base64decode(keyData.getNonceData())); + try { + recoveredKey = CryptoUtil.unwrapUsingSymmetricKey(token, ivps_server, + Utils.base64decode(wrappedRecoveredKey), + recoveryKey, EncryptionAlgorithm.DES3_CBC_PAD); + } catch (Exception e) { + log("Exception in unwrapping key: " + e.toString()); + e.printStackTrace(); + } + + if (!recoveredKey.equals(Utils.base64encode(vek.getEncoded()))) { + log("Error: recovered and archived keys do not match!"); + } else { + log("Success: recoverd and archived keys match!"); + } + + // Test 9: Submit a recovery request for the symmetric key using a passphrase + log("Submitting a recovery request for the symmetric key using a passphrase"); + recoveryPassphrase = "Gimme me keys please"; + + try { + recoveryKey = CryptoUtil.generateKey(token, KeyGenAlgorithm.DES3); + wrappedRecoveryPassphrase = CryptoUtil.wrapPassphrase(token, recoveryPassphrase, ivps, recoveryKey, + EncryptionAlgorithm.DES3_CBC_PAD); + wrappedRecoveryKey = CryptoUtil.wrapSymmetricKey(manager, token, transportCert, recoveryKey); + + requestInfo = client.requestRecovery(keyId, wrappedRecoveryPassphrase, wrappedRecoveryKey, ivps.getIV()); + recoveryRequestId = requestInfo.getRequestId(); + } catch (Exception e) { + log("Exception in recovering symmetric key using passphrase" + e.toString()); + e.printStackTrace(); + } + + //Test 10: Approve recovery + log("Approving recovery request: " + recoveryRequestId); + client.approveRecovery(recoveryRequestId); + + // Test 11: Get key + log("Getting key: " + keyId); + keyData = client.retrieveKey(keyId, recoveryRequestId, wrappedRecoveryPassphrase, wrappedRecoveryKey, ivps.getIV()); + wrappedRecoveredKey = keyData.getWrappedPrivateData(); + + try { + recoveredKey = CryptoUtil.unwrapUsingPassphrase(wrappedRecoveredKey, recoveryPassphrase); + } catch (Exception e) { + log("Error: unable to unwrap key using passphrase"); + e.printStackTrace(); + } + + if (recoveredKey == null || !recoveredKey.equals(Utils.base64encode(vek.getEncoded()))) { + log("Error: recovered and archived keys do not match!"); + } else { + log("Success: recovered and archived keys do match!"); + } + + + passphrase = "secret12345"; + // Test 12: Generate and archive a passphrase + clientId = "UUID: 123-45-6789 RKEK " + Calendar.getInstance().getTime().toString(); + try { + byte[] encoded = CryptoUtil.createPKIArchiveOptions(manager, token, transportCert, null, passphrase, + KeyGenAlgorithm.DES3, ivps); + requestInfo = client.archiveSecurityData(encoded, clientId, KeyRequestResource.PASS_PHRASE_TYPE); + log("Archival Results:"); + printRequestInfo(requestInfo); + keyId = requestInfo.getKeyId(); + } catch (Exception e) { + log("Exception in archiving symmetric key:" + e.toString()); + e.printStackTrace(); + } + + //Test 13: Get keyId for active passphrase with client ID + log("Getting key ID for passphrase"); + keyInfo = client.getKeyData(clientId, "active"); + keyId2 = keyInfo.getKeyId(); + if (keyId2 == null) { + log("No archived key found"); + } else { + log("Archived Key found: " + keyId); + } + + if (!keyId.equals(keyId2)) { + log("Error: key ids from search and archival do not match"); + } else { + log("Success: key ids from search and archival do match!"); + } + + // Test 14: Submit a recovery request for the passphrase using a session key + log("Submitting a recovery request for the passphrase using session key"); + recoveryKey = null; + recoveryRequestId = null; + wrappedRecoveryKey = null; + try { + recoveryKey = CryptoUtil.generateKey(token, KeyGenAlgorithm.DES3); + wrappedRecoveryKey = CryptoUtil.wrapSymmetricKey(manager, token, transportCert, recoveryKey); + wrappedRecoveryPassphrase = CryptoUtil.wrapPassphrase(token, recoveryPassphrase, ivps, recoveryKey, + EncryptionAlgorithm.DES3_CBC_PAD); + requestInfo = client.requestRecovery(keyId, null, wrappedRecoveryKey, ivps.getIV()); + recoveryRequestId = requestInfo.getRequestId(); + } catch (Exception e) { + log("Exception in recovering passphrase using session key: " + e.getMessage()); + } + + // Test 15: Approve recovery + log("Approving recovery request: " + recoveryRequestId); + client.approveRecovery(recoveryRequestId); + + // Test 16: Get key + log("Getting passphrase: " + keyId); + + keyData = client.retrieveKey(keyId, recoveryRequestId, null, wrappedRecoveryKey, ivps.getIV()); + wrappedRecoveredKey = keyData.getWrappedPrivateData(); + ivps_server = new IVParameterSpec( Utils.base64decode(keyData.getNonceData())); + try { + recoveredKey = CryptoUtil.unwrapUsingSymmetricKey(token, ivps_server, + Utils.base64decode(wrappedRecoveredKey), + recoveryKey, EncryptionAlgorithm.DES3_CBC_PAD); + recoveredKey = new String(Utils.base64decode(recoveredKey), "UTF-8"); + } catch (Exception e) { + log("Exception in unwrapping key: " + e.toString()); + e.printStackTrace(); + } + + if (recoveredKey == null || !recoveredKey.equals(passphrase)) { + log("Error: recovered and archived passphrases do not match!"); + } else { + log("Success: recovered and archived passphrases do match!"); + } + + // Test 17: Submit a recovery request for the passphrase using a passphrase + log("Submitting a recovery request for the passphrase using a passphrase"); + requestInfo = client.requestRecovery(keyId, wrappedRecoveryPassphrase, wrappedRecoveryKey, ivps.getIV()); + recoveryRequestId = requestInfo.getRequestId(); + + //Test 18: Approve recovery + log("Approving recovery request: " + recoveryRequestId); + client.approveRecovery(recoveryRequestId); + + // Test 19: Get key + log("Getting passphrase: " + keyId); + keyData = client.retrieveKey(keyId, recoveryRequestId, wrappedRecoveryPassphrase, wrappedRecoveryKey, ivps.getIV()); + wrappedRecoveredKey = keyData.getWrappedPrivateData(); + try { + recoveredKey = CryptoUtil.unwrapUsingPassphrase(wrappedRecoveredKey, recoveryPassphrase); + recoveredKey = new String(Utils.base64decode(recoveredKey), "UTF-8"); + } catch (Exception e) { + log("Error: cannot unwrap key using passphrase"); + e.printStackTrace(); + } + + if (recoveredKey == null || !recoveredKey.equals(passphrase)) { + log("Error: recovered and archived passphrases do not match!"); + } else { + log("Success: recovered and archived passphrases do match!"); + } + + // Test 20: Submit a recovery request for the passphrase using a passphrase + //Wait until retrieving key before sending input data. + + log("Submitting a recovery request for the passphrase using a passphrase, wait till end to provide recovery data."); + requestInfo = client.requestRecovery(keyId, null, null, null); + recoveryRequestId = requestInfo.getRequestId(); + + //Test 21: Approve recovery + log("Approving recovery request: " + recoveryRequestId); + client.approveRecovery(recoveryRequestId); + + // Test 22: Get key + log("Getting passphrase: " + keyId); + keyData = client.retrieveKey(keyId, recoveryRequestId, wrappedRecoveryPassphrase, wrappedRecoveryKey, ivps.getIV()); + wrappedRecoveredKey = keyData.getWrappedPrivateData(); + try { + recoveredKey = CryptoUtil.unwrapUsingPassphrase(wrappedRecoveredKey, recoveryPassphrase); + recoveredKey = new String(Utils.base64decode(recoveredKey), "UTF-8"); + } catch (Exception e) { + log("Error: Can't unwrap recovered key using passphrase"); + e.printStackTrace(); + } + + if (recoveredKey == null || !recoveredKey.equals(passphrase)) { + log("Error: recovered and archived passphrases do not match!"); + } else { + log("Success: recovered and archived passphrases do match!"); + } + } + + private static void log(String string) { + // TODO Auto-generated method stub + System.out.println(string); + } + + private static void printRequestInfo(KeyRequestInfo info) { + log("KeyRequestURL: " + info.getRequestURL()); + log("Key URL: " + info.getKeyURL()); + log("Status: " + info.getRequestStatus()); + log("Type: " + info.getRequestType()); + } + + //Use this when we actually create random initialization vectors + private static IVParameterSpec genIV(int blockSize) throws Exception { + // generate an IV + byte[] iv = new byte[blockSize]; + + Random rnd = new Random(); + rnd.nextBytes(iv); + + return new IVParameterSpec(iv); + } +} diff --git a/base/kra/functional/src/com/netscape/cms/servlet/test/GeneratePKIArchiveOptions.java b/base/kra/functional/src/com/netscape/cms/servlet/test/GeneratePKIArchiveOptions.java new file mode 100644 index 000000000..604430b57 --- /dev/null +++ b/base/kra/functional/src/com/netscape/cms/servlet/test/GeneratePKIArchiveOptions.java @@ -0,0 +1,222 @@ +package com.netscape.cms.servlet.test; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; +import java.io.CharConversionException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.cli.PosixParser; +import org.mozilla.jss.CryptoManager; +import org.mozilla.jss.asn1.InvalidBERException; +import org.mozilla.jss.crypto.AlreadyInitializedException; +import org.mozilla.jss.crypto.BadPaddingException; +import org.mozilla.jss.crypto.CryptoToken; +import org.mozilla.jss.crypto.IVParameterSpec; +import org.mozilla.jss.crypto.IllegalBlockSizeException; +import org.mozilla.jss.crypto.KeyGenAlgorithm; +import org.mozilla.jss.crypto.SymmetricKey; +import org.mozilla.jss.crypto.SymmetricKey.NotExtractableException; +import org.mozilla.jss.crypto.TokenException; +import org.mozilla.jss.util.Password; + +import com.netscape.cmsutil.crypto.CryptoUtil; +import com.netscape.cmsutil.util.Utils; + +@SuppressWarnings("deprecation") +public class GeneratePKIArchiveOptions { + + public static void usage(Options options) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("GeneratePKIArchiveOptions", options); + System.exit(1); + } + + private static void log(String string) { + // TODO Auto-generated method stub + System.out.println(string); + } + + // read in byte array + // we must do this somewhere? + public static byte[] read(String fname) throws IOException { + File file = new File(fname); + byte[] result = new byte[(int) file.length()]; + InputStream input = null; + try { + int totalBytesRead = 0; + input = new BufferedInputStream(new FileInputStream(file)); + while (totalBytesRead < result.length) { + int bytesRemaining = result.length - totalBytesRead; + //input.read() returns -1, 0, or more : + int bytesRead = input.read(result, totalBytesRead, bytesRemaining); + if (bytesRead > 0) { + totalBytesRead = totalBytesRead + bytesRead; + } + } + } catch (Exception e) { + throw new IOException(e); + } finally { + if (input != null) { + input.close(); + } + } + + return result; + } + + public static void write(byte[] aInput, String outFile) throws IOException { + OutputStream output = null; + try { + output = new BufferedOutputStream(new FileOutputStream(outFile)); + output.write(aInput); + } catch (Exception e) { + throw new IOException(e); + } finally { + if (output != null) { + output.close(); + } + } + } + + private static void write_file(String data, String outFile) throws IOException { + FileWriter fstream = new FileWriter(outFile); + BufferedWriter out = new BufferedWriter(fstream); + out.write(data); + //Close the output stream + out.close(); + } + + public static void main(String args[]) throws InvalidKeyException, CertificateEncodingException, + CharConversionException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, + IllegalStateException, TokenException, IOException, IllegalBlockSizeException, BadPaddingException, + InvalidBERException, NotExtractableException { + String token_pwd = null; + String db_dir = "./"; + String out_file = "./options.out"; + String transport_file = "./transport.crt"; + String key_file = "./symkey.out"; + String passphrase = null; + boolean passphraseMode = false; + + // parse command line arguments + Options options = new Options(); + options.addOption("w", true, "Token password (required)"); + options.addOption("d", true, "Directory for tokendb"); + options.addOption("p", true, "Passphrase"); + options.addOption("t", true, "File with transport cert"); + options.addOption("o", true, "Output file"); + options.addOption("k", true, "Key file"); + + try { + CommandLineParser parser = new PosixParser(); + CommandLine cmd = parser.parse(options, args); + + if (cmd.hasOption("p")) { + passphrase = cmd.getOptionValue("p"); + passphraseMode = true; + } + + if (cmd.hasOption("o")) { + out_file = cmd.getOptionValue("o"); + } + + if (cmd.hasOption("k")) { + key_file = cmd.getOptionValue("k"); + } + + if (cmd.hasOption("t")) { + transport_file = cmd.getOptionValue("t"); + } + + if (cmd.hasOption("w")) { + token_pwd = cmd.getOptionValue("w"); + } else { + System.err.println("Error: no token password provided"); + usage(options); + } + + if (cmd.hasOption("d")) { + db_dir = cmd.getOptionValue("d"); + } + + } catch (ParseException e) { + System.err.println("Error in parsing command line options: " + e.getMessage()); + usage(options); + } + + // used for crypto operations + byte iv[] = { 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1 }; + IVParameterSpec ivps = new IVParameterSpec(iv); + CryptoManager manager = null; + CryptoToken token = null; + + // used for wrapping to send data to DRM + byte[] tcert = read(transport_file); + String transportCert = Utils.base64encode(tcert); + + // Initialize token + try { + CryptoManager.initialize(db_dir); + } catch (AlreadyInitializedException e) { + // it is ok if it is already initialized + } catch (Exception e) { + log("INITIALIZATION ERROR: " + e.toString()); + System.exit(1); + } + + // log into token + try { + manager = CryptoManager.getInstance(); + token = manager.getInternalKeyStorageToken(); + Password password = new Password(token_pwd.toCharArray()); + try { + token.login(password); + } catch (Exception e) { + log("login Exception: " + e.toString()); + if (!token.isLoggedIn()) { + token.initPassword(password, password); + } + } + } catch (Exception e) { + log("Exception in logging into token:" + e.toString()); + } + + // Data to be archived + SymmetricKey vek = null; + if (!passphraseMode) { + vek = CryptoUtil.generateKey(token, KeyGenAlgorithm.DES3); + // store vek in file + write_file(Utils.base64encode(vek.getKeyData()), key_file); + } + + byte[] encoded = null; + + if (passphraseMode) { + encoded = CryptoUtil.createPKIArchiveOptions(manager, token, transportCert, null, passphrase, + KeyGenAlgorithm.DES3, ivps); + } else { + encoded = CryptoUtil.createPKIArchiveOptions(manager, token, transportCert, vek, null, + KeyGenAlgorithm.DES3, ivps); + } + + // write encoded to file + write_file(Utils.base64encode(encoded), out_file); + + } +} |