Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
simple-mbed-cloud-client/mbed-cloud-client/certificate-enrollment-client/source/CertificateEnrollmentClient.cpp
- Committer:
- leothedragon
- Date:
- 2021-05-04
- Revision:
- 0:8f0bb79ddd48
File content as of revision 0:8f0bb79ddd48:
// ---------------------------------------------------------------------------- // Copyright 2018 ARM Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ---------------------------------------------------------------------------- #include "CertificateEnrollmentClient.h" #include "mbed-client/m2mresource.h" #include "mbed-client/m2minterfacefactory.h" #include "mbed-client/m2minterface.h" #include "pal.h" #include "eventOS_scheduler.h" #include "eventOS_event.h" #include "ce_defs.h" #include "ce_tlv.h" #include "certificate_enrollment.h" #include <stdio.h> #include <string.h> #include "CertificateEnrollmentClient.h" #include "CertificateEnrollmentClientCommon.h" #include "CertificateRenewalData.h" #include "pv_error_handling.h" #include "pv_macros.h" #define RESOURCE_ID_CERTIFICATE_NAME "27002" #define OBJECT_LWM2M_CERTIFICATE "35011" #define NUMBER_OF_CONCURRENT_RENEWALS 1 /************************************************************************/ /* Different calls to update */ /************************************************************************/ extern const char g_lwm2m_name[]; namespace CertificateEnrollmentClient { // Event type that is part of the arm_event_s structure. enum event_type_e { EVENT_TYPE_INIT, // Some initializer - nothing done currently, initialization called by mbed cloud client EVENT_TYPE_RENEWAL_REQUEST, // Certificate renewal request. We can tell if it is initiated by the server or the device with the derived type of CertificateRenewalDataBase of the certificate descriptor / global variable EVENT_TYPE_EST_RESPONDED, // Certificate arrived from EST service, or EST failure EVENT_TYPE_MAX = 0xff // Must fit in a uint8_t (field in the arm_event_s struct) }; // Pointer to the EST client object for dealing with the EST service. extern const CERT_ENROLLMENT_EST_CLIENT *g_est_client; // Data for certificate that is currently being renewed. Will change to list if we need to support multiple renewals static CertificateRenewalDataBase *current_cert = NULL; // ID of the handler we register to the MbedCloudClient event loop static int8_t handler_id = -1; // Flag that indicates whether the module is initialized static bool is_initialized = false; // Semaphore for enforcing that only NUMBER_OF_CONCURRENT_RENEWALS (currently 1) request at a time may update current_cert. Hold lock until process finished. // Important: When pal_osSemaphoreWait called from within event loop - do not block, must set timeout to 0, and fail if failed to acquire lock // Future: For supporting renewals of NUMBER_OF_CONCURRENT_RENEWALS certificates simultaneously - change to semaphore that counts to NUMBER_OF_CONCURRENT_RENEWALS // and maintain a list of certificates of size NUMBER_OF_CONCURRENT_RENEWALS inside the event loop. static palSemaphoreID_t g_renewal_sem = 0; /** * \brief Finish the renewal process. * Zero current_cert pointer, then release the semaphore. Note that when the semaphore is released - new device renewals may be made. * Then call renewal_data->finish() and delete renewal_data. * * \param renewal_data the data of the certificate to be renewed. * It is important that this is passed to the function because after releasing the semaphore - the global pointer may be replaced. * \param exit_status the status of the renewal process */ static void certificate_renewal_finish(CertificateRenewalDataBase *renewal_data, ce_status_e exit_status); /** * \brief The function that handles all the CertificateEnrollmentClient events * Create an arm_event_s object and call eventOS_event_send() * The event will have an application level priority, and will be executed when in the head of the event queue. * In the future: an extra arg should be passed - some descriptor for the specific CertificateRenewalDataBase object. * * \param event_type An event identifier */ static void event_handler(arm_event_s* event); /** * \brief Send a new event to the event loop queue. * Create an arm_event_s object and call eventOS_event_send() * The event will have an application level priority * * \param renewal_data A pointer to an object derived from CertificateRenewalDataBase * \param event_type An event identifier */ static ce_status_e schedule_event(event_type_e event_type); /** * \brief Callback that will be executed when an EST service response is available * Event Context is probably network so this function will check if response is success, if so, allocate the data if needed in renewal_data->est_data. schedule a new event with type EVENT_TYPE_EST_RESPONDED. * * \param result Whether the EST client successfully received a certificate from the EST service * \param cert_chain structure containing the certificate/chain received from the EST service * \param Context passed when requesting a certificate via the EST client. Currently unused. In the future will be used to identify the relevant CertificateRenewalDataBase object. */ static void est_cb(est_enrollment_result_e result, cert_chain_context_s *cert_chain, void *context); /** * \brief The function that handles the EST response. * * Called by event_handler(). renewal_data->est_data already exists and is valid. * Will perform a safe replacement of the certificate with the new certificate received from EST * Then it will free the EST chain and finish the renewal operation. * \param renewal_data A pointer to an object derived from CertificateRenewalDataBase */ static void est_response_process(CertificateRenewalDataBase *renewal_data); /** * \brief Create g_cert_enroll_lwm2m_obj, from the object create an object resource, and create the resources. Then push the object to the MCC object list * * Note that the pointers to the objects created by this function is owned by the CertificateEnrollmentClient Module and must be released in by CertificateEnrollmentClient::finalize() * \param list A reference to the MbedCloudClient object list. MbedCloudClient will later set the resource */ static ce_status_e init_objects(M2MBaseList& list); /** * \brief Release the objects created by init_objects() * */ static void release_objects(); // Callback is called when we get a POST message to g_cert_enroll_lwm2m_resource (runs in high priority context!) /** * \brief Callback is called when we get a POST message to g_cert_enroll_lwm2m_resource * Runs in network context of the event loop. * This function extracts the input data, creates a CertificateEnrollmentClient::CertificateRenewalDataFromServer object sets the global variable * * \param arg a M2MResource::M2MExecuteParameter argument. */ static void certificate_renewal_post(void *arg); /** * \brief Start the renewal process. * Parse the certificate name, generate keys and CSR. Then call the EST client so the new certificate may be retrieved * * \param renewal_data the data of the certificate to be renewed */ static void certificate_renewal_start(CertificateRenewalDataBase *renewal_data); /** * \brief Call the user callback and send a response to the server, when a CertificateRenewalDataFromServer object does not exist. * Use only for server initiated renewal, since this sends a response to the server. * * \param tlv the raw data from the server - should be in the form of TLV * \param tlv_size size of the TLV buffer * \param ret_status The return status to return to the user callback and the server */ static void call_user_cb_send_response(const uint8_t *tlv, uint16_t tlv_size, ce_status_e ret_status); #ifdef CERT_RENEWAL_TEST void testonly_certificate_renewal_post(void *arg); #endif // CERT_RENEWAL_TEST } // FIXME: print error void CertificateEnrollmentClient::call_user_cb_send_response(const uint8_t *tlv, uint16_t tlv_size, ce_status_e ret_status) { CertificateEnrollmentClient::CertificateRenewalDataFromServer temp_obj(tlv, tlv_size); (void)temp_obj.parse(); // Call user callback with appropriate error // In case of parsing error (malformed TLV), the provided ret_status will be returned and not call_user_cert_renewal_cb(temp_obj.cert_name, ret_status, CE_INITIATOR_SERVER); // Send response to the server SA_PV_LOG_INFO("sending delayed response\n"); g_cert_enroll_lwm2m_resource->set_value(ret_status); g_cert_enroll_lwm2m_resource->send_delayed_post_response(); } void CertificateEnrollmentClient::certificate_renewal_post(void *arg) { palStatus_t pal_status; ce_status_e status; SA_PV_LOG_INFO_FUNC_ENTER_NO_ARGS(); M2MResource::M2MExecuteParameter *args = (M2MResource::M2MExecuteParameter *)arg; const uint8_t *data = args->get_argument_value(); const uint16_t data_size = args->get_argument_value_length(); // If CEC module is not initialized - do not even take semaphore - exit with proper response SA_PV_ERR_RECOVERABLE_RETURN_IF((!is_initialized), call_user_cb_send_response(data, data_size, CE_STATUS_NOT_INITIALIZED), "Certificate Renewal module not initialized"); pal_status = pal_osSemaphoreWait(g_renewal_sem, 0, NULL); if (pal_status == PAL_SUCCESS) { CertificateEnrollmentClient::current_cert = new CertificateEnrollmentClient::CertificateRenewalDataFromServer(data, data_size); if (!CertificateEnrollmentClient::current_cert) { status = CE_STATUS_OUT_OF_MEMORY; pal_status = pal_osSemaphoreRelease(g_renewal_sem); if (PAL_SUCCESS != pal_status) { // Should never happen status = CE_STATUS_ERROR; } call_user_cb_send_response(data, data_size, status); return; } // Enqueue the event status = schedule_event(CertificateEnrollmentClient::EVENT_TYPE_RENEWAL_REQUEST); SA_PV_ERR_RECOVERABLE_RETURN_IF((status != CE_STATUS_SUCCESS), certificate_renewal_finish(CertificateEnrollmentClient::current_cert, status), "Error scheduling event"); } else { SA_PV_LOG_ERR("Failed to take semaphore- device busy\n"); if (pal_status == PAL_ERR_RTOS_TIMEOUT) { status = CE_STATUS_DEVICE_BUSY; } else { status = CE_STATUS_ERROR; } call_user_cb_send_response(data, data_size, status); return; } SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); } ce_status_e CertificateEnrollmentClient::certificate_renew(const char *cert_name) { palStatus_t pal_status = PAL_SUCCESS; ce_status_e status = CE_STATUS_SUCCESS; SA_PV_ERR_RECOVERABLE_RETURN_IF((!cert_name), CE_STATUS_INVALID_PARAMETER, "Provided NULL certificate name"); SA_PV_ERR_RECOVERABLE_RETURN_IF((!is_initialized), CE_STATUS_NOT_INITIALIZED, "Certificate Renewal module not initialized"); SA_PV_LOG_INFO_FUNC_ENTER("cert_name = %s\n", cert_name); pal_status = pal_osSemaphoreWait(g_renewal_sem, 0, NULL); if (pal_status == PAL_SUCCESS) { CertificateEnrollmentClient::current_cert = new CertificateEnrollmentClient::CertificateRenewalDataFromDevice(cert_name); SA_PV_ERR_RECOVERABLE_GOTO_IF((!CertificateEnrollmentClient::current_cert), status = CE_STATUS_OUT_OF_MEMORY, ReleseSemReturn, "Allocation error"); // Enqueue the event status = schedule_event(CertificateEnrollmentClient::EVENT_TYPE_RENEWAL_REQUEST); SA_PV_ERR_RECOVERABLE_GOTO_IF((status != CE_STATUS_SUCCESS), status = status, ReleseSemReturn, "Error scheduling event"); // If some error synchronous error has occurred before scheduling the event - release the semaphore we had just taken, // and then return the error without calling the user callback ReleseSemReturn: if (status != CE_STATUS_SUCCESS) { pal_status = pal_osSemaphoreRelease(g_renewal_sem); if (PAL_SUCCESS != pal_status) { // Should never happen status = CE_STATUS_ERROR; } } } else { // return with appropriate error if (pal_status == PAL_ERR_RTOS_TIMEOUT) { status = CE_STATUS_DEVICE_BUSY; } else { status = CE_STATUS_ERROR; } } SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); return status; } void CertificateEnrollmentClient::on_certificate_renewal(cert_renewal_cb_f user_cb) { SA_PV_LOG_INFO_FUNC_ENTER_NO_ARGS(); CertificateEnrollmentClient::set_user_cert_renewal_cb(user_cb); SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); } ce_status_e CertificateEnrollmentClient::init_objects(M2MBaseList& list) { M2MObjectInstance *cert_enroll_lwm2m_obj_instance; ce_status_e ce_status = CE_STATUS_SUCCESS; SA_PV_LOG_INFO_FUNC_ENTER_NO_ARGS(); // Create the certificate enrollment resource g_cert_enroll_lwm2m_obj = M2MInterfaceFactory::create_object(OBJECT_LWM2M_CERTIFICATE); SA_PV_ERR_RECOVERABLE_RETURN_IF((!g_cert_enroll_lwm2m_obj), CE_STATUS_ERROR, "Error creating LWM2M object"); // Create the instance cert_enroll_lwm2m_obj_instance = g_cert_enroll_lwm2m_obj->create_object_instance(); SA_PV_ERR_RECOVERABLE_GOTO_IF((!cert_enroll_lwm2m_obj_instance), ce_status = CE_STATUS_ERROR, Cleanup, "Error creating LWM2M object instance"); // Create the resource g_cert_enroll_lwm2m_resource = cert_enroll_lwm2m_obj_instance->create_dynamic_resource(RESOURCE_ID_CERTIFICATE_NAME, "Enroll", M2MResourceInstance::INTEGER, false); SA_PV_ERR_RECOVERABLE_GOTO_IF((!g_cert_enroll_lwm2m_resource), ce_status = CE_STATUS_ERROR, Cleanup, "Error creating LWM2M resource"); // Allow POST operations g_cert_enroll_lwm2m_resource->set_operation(M2MBase::POST_ALLOWED); // Set the resource callback SA_PV_ERR_RECOVERABLE_GOTO_IF((!g_cert_enroll_lwm2m_resource->set_execute_function(CertificateEnrollmentClient::certificate_renewal_post)), ce_status = CE_STATUS_ERROR, Cleanup, "Error resource callback"); // Enable sending of delayed responses g_cert_enroll_lwm2m_resource->set_delayed_response(true); // Push the object to the list list.push_back(g_cert_enroll_lwm2m_obj); Cleanup: if (ce_status != CE_STATUS_SUCCESS) { // Destroying the object will destroy all instances and resources associated with it delete g_cert_enroll_lwm2m_obj; g_cert_enroll_lwm2m_resource = NULL; } SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); return ce_status; } void CertificateEnrollmentClient::release_objects() { delete g_cert_enroll_lwm2m_obj; g_cert_enroll_lwm2m_obj = NULL; } ce_status_e CertificateEnrollmentClient::init(M2MBaseList& list, const EstClient *est_client) { ce_status_e ce_status = CE_STATUS_SUCCESS; palStatus_t pal_status = PAL_SUCCESS; SA_PV_LOG_INFO_FUNC_ENTER_NO_ARGS(); if (!is_initialized) { // Init the LWM2M object and resource and push the object ce_status = init_objects(list); SA_PV_ERR_RECOVERABLE_RETURN_IF((ce_status != CE_STATUS_SUCCESS), ce_status, "Error initializing LWM2M object and resource"); // Put the handler creation in a critical code block for the case that this function is called after the start of the event loop eventOS_scheduler_mutex_wait(); if (handler_id == -1) { // Register the handler only if it hadn't been registered before handler_id = eventOS_event_handler_create(CertificateEnrollmentClient::event_handler, EVENT_TYPE_INIT); SA_PV_ERR_RECOVERABLE_RETURN_IF((handler_id == -1), ce_status, "Error creating event handler"); } eventOS_scheduler_mutex_release(); // Initialize the CE module ce_status = ce_init(); SA_PV_ERR_RECOVERABLE_RETURN_IF((ce_status != CE_STATUS_SUCCESS), ce_status, "Error initializing CE module"); // Create the certificate renewal mutex pal_status = pal_osSemaphoreCreate(NUMBER_OF_CONCURRENT_RENEWALS, &g_renewal_sem); SA_PV_ERR_RECOVERABLE_RETURN_IF((pal_status != PAL_SUCCESS), CE_STATUS_ERROR, "Error creating semaphore"); #ifdef CERT_ENROLLMENT_EST_MOCK PV_UNUSED_PARAM(est_client); g_est_client = new EstClientMock(); SA_PV_ERR_RECOVERABLE_RETURN_IF((!g_est_client), CE_STATUS_ERROR, "Error creating mock EST"); #else g_est_client = est_client; #endif is_initialized = true; } SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); return CE_STATUS_SUCCESS; } void CertificateEnrollmentClient::finalize() { palStatus_t pal_status; // If module not initialized - do nothing if (is_initialized) { pal_status = pal_osSemaphoreDelete(&g_renewal_sem); if (pal_status != PAL_SUCCESS) { SA_PV_LOG_ERR("Error deleting semaphore"); } #ifdef CERT_ENROLLMENT_EST_MOCK delete g_est_client; #endif is_initialized = false; // LWM2M objects, instances, and resources are deleted when MbedCloudClient is unregistered and ServiceClient::state_unregister() is called // Currently nothing to finalize for CE core module except for KCM. However we do not wish to finalize it it may be used by other resources // Release our resources release_objects(); } } void CertificateEnrollmentClient::event_handler(arm_event_s* event) { SA_PV_LOG_INFO_FUNC_ENTER_NO_ARGS(); switch (event->event_type) { case EVENT_TYPE_INIT: // Nothing to do - ce module already initialized break; case EVENT_TYPE_RENEWAL_REQUEST: certificate_renewal_start(current_cert); break; case EVENT_TYPE_EST_RESPONDED: est_response_process(current_cert); break; default: // Should never happen SA_PV_LOG_ERR("Unsuupported event\n"); } SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); } // This is the entry point of the renewal request, inside the event loop void CertificateEnrollmentClient::certificate_renewal_start(CertificateRenewalDataBase *renewal_data) { ce_status_e ce_status; est_status_e est_status; const char *cert_name; size_t cert_name_size; kcm_status_e kcm_status; SA_PV_LOG_INFO_FUNC_ENTER_NO_ARGS(); // Parse the certificate name ce_status = renewal_data->parse(); SA_PV_ERR_RECOVERABLE_RETURN_IF((ce_status != CE_STATUS_SUCCESS), certificate_renewal_finish(renewal_data, ce_status), "Parse error"); // Create CSR's key handle kcm_status = cs_ec_key_new(&renewal_data->key_handle); // translate error to some CE native error ce_status = ce_error_handler(kcm_status); SA_PV_ERR_RECOVERABLE_RETURN_IF((ce_status != CE_STATUS_SUCCESS), certificate_renewal_finish(renewal_data, ce_status), "Failed creating new key handle"); // key handle is initialized in the base constructor ce_status = ce_generate_keys_and_create_csr_from_certificate(renewal_data->cert_name, renewal_data->key_handle, &renewal_data->csr, &renewal_data->csr_size); SA_PV_ERR_RECOVERABLE_RETURN_IF((ce_status != CE_STATUS_SUCCESS), certificate_renewal_finish(renewal_data, ce_status), "Keys/CSR generation error"); // Call the EST client // If lwm2m device certificate - set cert name to NULL and request EST enrollment if (pv_str_equals(g_lwm2m_name, renewal_data->cert_name,(uint32_t)(strlen(g_lwm2m_name) + 1))) { SA_PV_LOG_INFO("Attempting to renew LwM2M device certificate\n"); cert_name = NULL; cert_name_size = 0; } else { SA_PV_LOG_INFO("Attempting to renew a custom certificate\n"); cert_name = renewal_data->cert_name; cert_name_size = strlen(renewal_data->cert_name); } // Request a certificate from a CSR via the EST service est_status = g_est_client->est_request_enrollment(cert_name, cert_name_size, renewal_data->csr, renewal_data->csr_size, est_cb, NULL); // FIXME: Currently commented out. If we find that the CSR must be persistent only during est_request_enrollment call - uncomment, and this should be the only place we free the CSR //free(renewal_data->csr); SA_PV_ERR_RECOVERABLE_RETURN_IF((est_status != EST_STATUS_SUCCESS), certificate_renewal_finish(renewal_data, CE_STATUS_EST_ERROR), "EST request failed"); SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); } ce_status_e CertificateEnrollmentClient::schedule_event(event_type_e event_type) { int8_t event_status; arm_event_s event = { .receiver = handler_id, // ID we got when creating our handler .sender = 0, // Which tasklet sent us the event is irrelevant to us .event_type = event_type, // Indicate event type .event_id = 0, // We currently do not need an ID for a specific event - event type is enough .data_ptr = 0, // Not needed, data handled in internal structure .priority = ARM_LIB_LOW_PRIORITY_EVENT, // Application level priority .event_data = 0, // With one certificate this is irrelevant. If allow multiple certificates, This will be a certificate descriptor (index in a CertificateRenewalDataBase list) }; SA_PV_LOG_INFO_FUNC_ENTER_NO_ARGS(); event_status = eventOS_event_send(&event); SA_PV_ERR_RECOVERABLE_RETURN_IF((event_status < 0), CE_STATUS_OUT_OF_MEMORY, "Error scheduling event"); SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); return CE_STATUS_SUCCESS; } void CertificateEnrollmentClient::est_cb(est_enrollment_result_e result, cert_chain_context_s *cert_chain, void *context) { ce_status_e status; SA_PV_LOG_INFO_FUNC_ENTER("result = %d", result); PV_UNUSED_PARAM(context); if (result != EST_ENROLLMENT_SUCCESS || cert_chain == NULL) { return certificate_renewal_finish(current_cert, CE_STATUS_EST_ERROR); } // Cert chain remains persistent until g_est_client->free_cert_chain_context is called current_cert->est_data = cert_chain; status = schedule_event(CertificateEnrollmentClient::EVENT_TYPE_EST_RESPONDED); if (status != CE_STATUS_SUCCESS) { // If event scheduling fails - free the chain context and finish the process SA_PV_LOG_INFO("Error scheduling event"); g_est_client->free_cert_chain_context(current_cert->est_data); // Make sure we do not keep an invalid pointer current_cert->est_data = NULL; certificate_renewal_finish(current_cert, status); } SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); } void CertificateEnrollmentClient::est_response_process(CertificateRenewalDataBase *renewal_data) { ce_status_e ce_status; ce_renewal_params_s params; SA_PV_LOG_INFO_FUNC_ENTER_NO_ARGS(); // Fill params params.cert_data = renewal_data->est_data; params.crypto_handle = renewal_data->key_handle; // Perform a safe renewal ce_status = ce_safe_renewal(renewal_data->cert_name, ¶ms); // Free the est chain. Do not free in the destructor, we'd rather free it as soon as possible g_est_client->free_cert_chain_context(renewal_data->est_data); renewal_data->est_data = NULL; // Done! certificate_renewal_finish(renewal_data, ce_status); SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); } void CertificateEnrollmentClient::certificate_renewal_finish(CertificateRenewalDataBase *renewal_data, ce_status_e exit_status) { palStatus_t pal_status; SA_PV_LOG_INFO_FUNC_ENTER("exit_status = %d", exit_status); // Don't leave an invalid global pointer current_cert = NULL; // Note: release of the mutex is before the deletion of the object (which holds the allocated cert_name) // and before the user callback is invoked (so that the user may call the renewal API successfully from within his callback) pal_status = pal_osSemaphoreRelease(g_renewal_sem); if (PAL_SUCCESS != pal_status) { // exit_status = CE_STATUS_ERROR; } // At this point, new device requests may be made and the global pointer CertificateEnrollmentClient::current_cert may be changed. // Therefore, we use the renewal_data pointer that was past as a parameter to this function // New server requests will not be made until after this function returns since the response to the server is enqueued into the event loop by renewal_data->finish() // and it is guaranteed that the server will not send another request until it receives a response. renewal_data->finish(exit_status); delete renewal_data; SA_PV_LOG_INFO_FUNC_EXIT_NO_ARGS(); } #ifdef CERT_RENEWAL_TEST void CertificateEnrollmentClient::testonly_certificate_renewal_post(void *arg) { return certificate_renewal_post(arg); } #endif // CERT_RENEWAL_TEST