/* chronyd/chronyc - Programs for keeping computer clocks accurate. ********************************************************************** * Copyright (C) Miroslav Lichvar 2020-2021 * * This program is free software; you can redistribute it and/or modify * it under the terms of version 2 of the GNU General Public License as * published by the Free Software Foundation. * * 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. * ********************************************************************** ======================================================================= NTS-KE session used by server and client */ #include "config.h" #include "sysincl.h" #include "nts_ke_session.h" #include "conf.h" #include "local.h" #include "logging.h" #include "memory.h" #include "siv.h" #include "socket.h" #include "sched.h" #include "util.h" #include #include #define INVALID_SOCK_FD (-8) struct RecordHeader { uint16_t type; uint16_t body_length; }; struct Message { int length; int sent; int parsed; int complete; unsigned char data[NKE_MAX_MESSAGE_LENGTH]; }; typedef enum { KE_WAIT_CONNECT, KE_HANDSHAKE, KE_SEND, KE_RECEIVE, KE_SHUTDOWN, KE_STOPPED, } KeState; struct NKSN_Instance_Record { int server; char *server_name; NKSN_MessageHandler handler; void *handler_arg; KeState state; int sock_fd; char *label; gnutls_session_t tls_session; SCH_TimeoutID timeout_id; int retry_factor; struct Message message; int new_message; }; /* ================================================== */ static gnutls_priority_t priority_cache; static int credentials_counter = 0; static int clock_updates = 0; /* ================================================== */ static void reset_message(struct Message *message) { message->length = 0; message->sent = 0; message->parsed = 0; message->complete = 0; } /* ================================================== */ static int add_record(struct Message *message, int critical, int type, const void *body, int body_length) { struct RecordHeader header; assert(message->length <= sizeof (message->data)); if (body_length < 0 || body_length > 0xffff || type < 0 || type > 0x7fff || message->length + sizeof (header) + body_length > sizeof (message->data)) return 0; header.type = htons(!!critical * NKE_RECORD_CRITICAL_BIT | type); header.body_length = htons(body_length); memcpy(&message->data[message->length], &header, sizeof (header)); message->length += sizeof (header); if (body_length > 0) { memcpy(&message->data[message->length], body, body_length); message->length += body_length; } return 1; } /* ================================================== */ static void reset_message_parsing(struct Message *message) { message->parsed = 0; } /* ================================================== */ static int get_record(struct Message *message, int *critical, int *type, int *body_length, void *body, int buffer_length) { struct RecordHeader header; int blen, rlen; if (message->length < message->parsed + sizeof (header) || buffer_length < 0) return 0; memcpy(&header, &message->data[message->parsed], sizeof (header)); blen = ntohs(header.body_length); rlen = sizeof (header) + blen; assert(blen >= 0 && rlen > 0); if (message->length < message->parsed + rlen) return 0; if (critical) *critical = !!(ntohs(header.type) & NKE_RECORD_CRITICAL_BIT); if (type) *type = ntohs(header.type) & ~NKE_RECORD_CRITICAL_BIT; if (body) memcpy(body, &message->data[message->parsed + sizeof (header)], MIN(buffer_length, blen)); if (body_length) *body_length = blen; message->parsed += rlen; return 1; } /* ================================================== */ static int check_message_format(struct Message *message, int eof) { int critical = 0, type = -1, length = -1, ends = 0; reset_message_parsing(message); message->complete = 0; while (get_record(message, &critical, &type, &length, NULL, 0)) { if (type == NKE_RECORD_END_OF_MESSAGE) { if (!critical || length != 0 || ends > 0) return 0; ends++; } } /* If the message cannot be fully parsed, but more data may be coming, consider the format to be ok */ if (message->length == 0 || message->parsed < message->length) return !eof; if (type != NKE_RECORD_END_OF_MESSAGE) return !eof; message->complete = 1; return 1; } /* ================================================== */ static gnutls_session_t create_tls_session(int server_mode, int sock_fd, const char *server_name, gnutls_certificate_credentials_t credentials, gnutls_priority_t priority) { unsigned char alpn_name[sizeof (NKE_ALPN_NAME)]; gnutls_session_t session; gnutls_datum_t alpn; unsigned int flags; int r; r = gnutls_init(&session, GNUTLS_NONBLOCK | GNUTLS_NO_TICKETS | (server_mode ? GNUTLS_SERVER : GNUTLS_CLIENT)); if (r < 0) { LOG(LOGS_ERR, "Could not %s TLS session : %s", "create", gnutls_strerror(r)); return NULL; } if (!server_mode) { assert(server_name); if (!UTI_IsStringIP(server_name)) { r = gnutls_server_name_set(session, GNUTLS_NAME_DNS, server_name, strlen(server_name)); if (r < 0) goto error; } flags = 0; if (clock_updates < CNF_GetNoCertTimeCheck()) { flags |= GNUTLS_VERIFY_DISABLE_TIME_CHECKS | GNUTLS_VERIFY_DISABLE_TRUSTED_TIME_CHECKS; DEBUG_LOG("Disabled time checks"); } gnutls_session_set_verify_cert(session, server_name, flags); } r = gnutls_priority_set(session, priority); if (r < 0) goto error; r = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, credentials); if (r < 0) goto error; memcpy(alpn_name, NKE_ALPN_NAME, sizeof (alpn_name)); alpn.data = alpn_name; alpn.size = sizeof (alpn_name) - 1; r = gnutls_alpn_set_protocols(session, &alpn, 1, 0); if (r < 0) goto error; gnutls_transport_set_int(session, sock_fd); return session; error: LOG(LOGS_ERR, "Could not %s TLS session : %s", "set", gnutls_strerror(r)); gnutls_deinit(session); return NULL; } /* ================================================== */ static void stop_session(NKSN_Instance inst) { if (inst->state == KE_STOPPED) return; inst->state = KE_STOPPED; SCH_RemoveFileHandler(inst->sock_fd); SCK_CloseSocket(inst->sock_fd); inst->sock_fd = INVALID_SOCK_FD; Free(inst->label); inst->label = NULL; gnutls_deinit(inst->tls_session); inst->tls_session = NULL; SCH_RemoveTimeout(inst->timeout_id); inst->timeout_id = 0; } /* ================================================== */ static void session_timeout(void *arg) { NKSN_Instance inst = arg; LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "NTS-KE session with %s timed out", inst->label); inst->timeout_id = 0; stop_session(inst); } /* ================================================== */ static int check_alpn(NKSN_Instance inst) { gnutls_datum_t alpn; if (gnutls_alpn_get_selected_protocol(inst->tls_session, &alpn) < 0 || alpn.size != sizeof (NKE_ALPN_NAME) - 1 || memcmp(alpn.data, NKE_ALPN_NAME, sizeof (NKE_ALPN_NAME) - 1) != 0) return 0; return 1; } /* ================================================== */ static void set_input_output(NKSN_Instance inst, int output) { SCH_SetFileHandlerEvent(inst->sock_fd, SCH_FILE_INPUT, !output); SCH_SetFileHandlerEvent(inst->sock_fd, SCH_FILE_OUTPUT, output); } /* ================================================== */ static void change_state(NKSN_Instance inst, KeState state) { int output; switch (state) { case KE_HANDSHAKE: output = !inst->server; break; case KE_WAIT_CONNECT: case KE_SEND: case KE_SHUTDOWN: output = 1; break; case KE_RECEIVE: output = 0; break; default: assert(0); } set_input_output(inst, output); inst->state = state; } /* ================================================== */ static int handle_event(NKSN_Instance inst, int event) { struct Message *message = &inst->message; int r; DEBUG_LOG("Session event %d fd=%d state=%d", event, inst->sock_fd, (int)inst->state); switch (inst->state) { case KE_WAIT_CONNECT: /* Check if connect() succeeded */ if (event != SCH_FILE_OUTPUT) return 0; /* Get the socket error */ if (!SCK_GetIntOption(inst->sock_fd, SOL_SOCKET, SO_ERROR, &r)) r = EINVAL; if (r != 0) { LOG(LOGS_ERR, "Could not connect to %s : %s", inst->label, strerror(r)); stop_session(inst); return 0; } DEBUG_LOG("Connected to %s", inst->label); change_state(inst, KE_HANDSHAKE); return 0; case KE_HANDSHAKE: r = gnutls_handshake(inst->tls_session); if (r < 0) { if (gnutls_error_is_fatal(r)) { gnutls_datum_t cert_error; /* Get a description of verification errors */ if (r != GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR || gnutls_certificate_verification_status_print( gnutls_session_get_verify_cert_status(inst->tls_session), gnutls_certificate_type_get(inst->tls_session), &cert_error, 0) < 0) cert_error.data = NULL; LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "TLS handshake with %s failed : %s%s%s", inst->label, gnutls_strerror(r), cert_error.data ? " " : "", cert_error.data ? (const char *)cert_error.data : ""); if (cert_error.data) gnutls_free(cert_error.data); stop_session(inst); /* Increase the retry interval if the handshake did not fail due to the other end closing the connection */ if (r != GNUTLS_E_PULL_ERROR && r != GNUTLS_E_PREMATURE_TERMINATION) inst->retry_factor = NKE_RETRY_FACTOR2_TLS; return 0; } /* Disable output when the handshake is trying to receive data */ set_input_output(inst, gnutls_record_get_direction(inst->tls_session)); return 0; } inst->retry_factor = NKE_RETRY_FACTOR2_TLS; if (DEBUG) { char *description = gnutls_session_get_desc(inst->tls_session); DEBUG_LOG("Handshake with %s completed %s", inst->label, description ? description : ""); gnutls_free(description); } if (!check_alpn(inst)) { LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "NTS-KE not supported by %s", inst->label); stop_session(inst); return 0; } /* Client will send a request to the server */ change_state(inst, inst->server ? KE_RECEIVE : KE_SEND); return 0; case KE_SEND: assert(inst->new_message && message->complete); assert(message->length <= sizeof (message->data) && message->length > message->sent); r = gnutls_record_send(inst->tls_session, &message->data[message->sent], message->length - message->sent); if (r < 0) { if (gnutls_error_is_fatal(r)) { LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "Could not send NTS-KE message to %s : %s", inst->label, gnutls_strerror(r)); stop_session(inst); } return 0; } DEBUG_LOG("Sent %d bytes to %s", r, inst->label); message->sent += r; if (message->sent < message->length) return 0; /* Client will receive a response */ change_state(inst, inst->server ? KE_SHUTDOWN : KE_RECEIVE); reset_message(&inst->message); inst->new_message = 0; return 0; case KE_RECEIVE: do { if (message->length >= sizeof (message->data)) { DEBUG_LOG("Message is too long"); stop_session(inst); return 0; } r = gnutls_record_recv(inst->tls_session, &message->data[message->length], sizeof (message->data) - message->length); if (r < 0) { /* Handle a renegotiation request on both client and server as a protocol error */ if (gnutls_error_is_fatal(r) || r == GNUTLS_E_REHANDSHAKE) { LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "Could not receive NTS-KE message from %s : %s", inst->label, gnutls_strerror(r)); stop_session(inst); } return 0; } DEBUG_LOG("Received %d bytes from %s", r, inst->label); message->length += r; } while (gnutls_record_check_pending(inst->tls_session) > 0); if (!check_message_format(message, r == 0)) { LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "Received invalid NTS-KE message from %s", inst->label); stop_session(inst); return 0; } /* Wait for more data if the message is not complete yet */ if (!message->complete) return 0; /* Server will send a response to the client */ change_state(inst, inst->server ? KE_SEND : KE_SHUTDOWN); /* Return success to process the received message */ return 1; case KE_SHUTDOWN: r = gnutls_bye(inst->tls_session, GNUTLS_SHUT_RDWR); if (r < 0) { if (gnutls_error_is_fatal(r)) { DEBUG_LOG("Shutdown with %s failed : %s", inst->label, gnutls_strerror(r)); stop_session(inst); return 0; } /* Disable output when the TLS shutdown is trying to receive data */ set_input_output(inst, gnutls_record_get_direction(inst->tls_session)); return 0; } SCK_ShutdownConnection(inst->sock_fd); stop_session(inst); DEBUG_LOG("Shutdown completed"); return 0; default: assert(0); return 0; } } /* ================================================== */ static void read_write_socket(int fd, int event, void *arg) { NKSN_Instance inst = arg; if (!handle_event(inst, event)) return; /* A valid message was received. Call the handler to process the message, and prepare a response if it is a server. */ reset_message_parsing(&inst->message); if (!(inst->handler)(inst->handler_arg)) { stop_session(inst); return; } } /* ================================================== */ static time_t get_time(time_t *t) { struct timespec now; LCL_ReadCookedTime(&now, NULL); if (t) *t = now.tv_sec; return now.tv_sec; } /* ================================================== */ static void handle_step(struct timespec *raw, struct timespec *cooked, double dfreq, double doffset, LCL_ChangeType change_type, void *anything) { if (change_type != LCL_ChangeUnknownStep && clock_updates < INT_MAX) clock_updates++; } /* ================================================== */ static int gnutls_initialised = 0; static int init_gnutls(void) { int r; if (gnutls_initialised) return 1; r = gnutls_global_init(); if (r < 0) LOG_FATAL("Could not initialise %s : %s", "gnutls", gnutls_strerror(r)); /* Prepare a priority cache for server and client NTS-KE sessions (the NTS specification requires TLS1.3 or later) */ r = gnutls_priority_init2(&priority_cache, "-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.2:-VERS-DTLS-ALL", NULL, GNUTLS_PRIORITY_INIT_DEF_APPEND); if (r < 0) { LOG(LOGS_ERR, "Could not initialise %s : %s", "priority cache for TLS", gnutls_strerror(r)); gnutls_global_deinit(); return 0; } /* Use our clock instead of the system clock in certificate verification */ gnutls_global_set_time_function(get_time); gnutls_initialised = 1; DEBUG_LOG("Initialised"); LCL_AddParameterChangeHandler(handle_step, NULL); return 1; } /* ================================================== */ static void deinit_gnutls(void) { if (!gnutls_initialised || credentials_counter > 0) return; LCL_RemoveParameterChangeHandler(handle_step, NULL); gnutls_priority_deinit(priority_cache); gnutls_global_deinit(); gnutls_initialised = 0; DEBUG_LOG("Deinitialised"); } /* ================================================== */ static NKSN_Credentials create_credentials(const char **certs, const char **keys, int n_certs_keys, const char **trusted_certs, uint32_t *trusted_certs_ids, int n_trusted_certs, uint32_t trusted_cert_set) { gnutls_certificate_credentials_t credentials = NULL; int i, r; if (!init_gnutls()) return NULL; r = gnutls_certificate_allocate_credentials(&credentials); if (r < 0) goto error; if (certs && keys) { if (trusted_certs || trusted_certs_ids) assert(0); for (i = 0; i < n_certs_keys; i++) { if (!UTI_CheckFilePermissions(keys[i], 0771)) ; r = gnutls_certificate_set_x509_key_file(credentials, certs[i], keys[i], GNUTLS_X509_FMT_PEM); if (r < 0) goto error; } } else { if (certs || keys || n_certs_keys > 0) assert(0); if (trusted_cert_set == 0 && !CNF_GetNoSystemCert()) { r = gnutls_certificate_set_x509_system_trust(credentials); if (r < 0) goto error; } if (trusted_certs && trusted_certs_ids) { for (i = 0; i < n_trusted_certs; i++) { struct stat buf; if (trusted_certs_ids[i] != trusted_cert_set) continue; if (stat(trusted_certs[i], &buf) == 0 && S_ISDIR(buf.st_mode)) r = gnutls_certificate_set_x509_trust_dir(credentials, trusted_certs[i], GNUTLS_X509_FMT_PEM); else r = gnutls_certificate_set_x509_trust_file(credentials, trusted_certs[i], GNUTLS_X509_FMT_PEM); if (r < 0) goto error; DEBUG_LOG("Added %d trusted certs from %s", r, trusted_certs[i]); } } } credentials_counter++; return (NKSN_Credentials)credentials; error: LOG(LOGS_ERR, "Could not set credentials : %s", gnutls_strerror(r)); if (credentials) gnutls_certificate_free_credentials(credentials); deinit_gnutls(); return NULL; } /* ================================================== */ NKSN_Credentials NKSN_CreateServerCertCredentials(const char **certs, const char **keys, int n_certs_keys) { return create_credentials(certs, keys, n_certs_keys, NULL, NULL, 0, 0); } /* ================================================== */ NKSN_Credentials NKSN_CreateClientCertCredentials(const char **certs, uint32_t *ids, int n_certs_ids, uint32_t trusted_cert_set) { return create_credentials(NULL, NULL, 0, certs, ids, n_certs_ids, trusted_cert_set); } /* ================================================== */ void NKSN_DestroyCertCredentials(NKSN_Credentials credentials) { gnutls_certificate_free_credentials((gnutls_certificate_credentials_t)credentials); credentials_counter--; deinit_gnutls(); } /* ================================================== */ NKSN_Instance NKSN_CreateInstance(int server_mode, const char *server_name, NKSN_MessageHandler handler, void *handler_arg) { NKSN_Instance inst; inst = MallocNew(struct NKSN_Instance_Record); inst->server = server_mode; inst->server_name = server_name ? Strdup(server_name) : NULL; inst->handler = handler; inst->handler_arg = handler_arg; /* Replace a NULL argument with the session itself */ if (!inst->handler_arg) inst->handler_arg = inst; inst->state = KE_STOPPED; inst->sock_fd = INVALID_SOCK_FD; inst->label = NULL; inst->tls_session = NULL; inst->timeout_id = 0; inst->retry_factor = NKE_RETRY_FACTOR2_CONNECT; return inst; } /* ================================================== */ void NKSN_DestroyInstance(NKSN_Instance inst) { stop_session(inst); Free(inst->server_name); Free(inst); } /* ================================================== */ int NKSN_StartSession(NKSN_Instance inst, int sock_fd, const char *label, NKSN_Credentials credentials, double timeout) { assert(inst->state == KE_STOPPED); inst->tls_session = create_tls_session(inst->server, sock_fd, inst->server_name, (gnutls_certificate_credentials_t)credentials, priority_cache); if (!inst->tls_session) return 0; inst->sock_fd = sock_fd; SCH_AddFileHandler(sock_fd, SCH_FILE_INPUT, read_write_socket, inst); inst->label = Strdup(label); inst->timeout_id = SCH_AddTimeoutByDelay(timeout, session_timeout, inst); inst->retry_factor = NKE_RETRY_FACTOR2_CONNECT; reset_message(&inst->message); inst->new_message = 0; change_state(inst, inst->server ? KE_HANDSHAKE : KE_WAIT_CONNECT); return 1; } /* ================================================== */ void NKSN_BeginMessage(NKSN_Instance inst) { reset_message(&inst->message); inst->new_message = 1; } /* ================================================== */ int NKSN_AddRecord(NKSN_Instance inst, int critical, int type, const void *body, int body_length) { assert(inst->new_message && !inst->message.complete); assert(type != NKE_RECORD_END_OF_MESSAGE); return add_record(&inst->message, critical, type, body, body_length); } /* ================================================== */ int NKSN_EndMessage(NKSN_Instance inst) { assert(!inst->message.complete); /* Terminate the message */ if (!add_record(&inst->message, 1, NKE_RECORD_END_OF_MESSAGE, NULL, 0)) return 0; inst->message.complete = 1; return 1; } /* ================================================== */ int NKSN_GetRecord(NKSN_Instance inst, int *critical, int *type, int *body_length, void *body, int buffer_length) { int type2; assert(inst->message.complete); if (body_length) *body_length = 0; if (!get_record(&inst->message, critical, &type2, body_length, body, buffer_length)) return 0; /* Hide the end-of-message record */ if (type2 == NKE_RECORD_END_OF_MESSAGE) return 0; if (type) *type = type2; return 1; } /* ================================================== */ int NKSN_GetKeys(NKSN_Instance inst, SIV_Algorithm siv, NKE_Key *c2s, NKE_Key *s2c) { int length = SIV_GetKeyLength(siv); if (length <= 0 || length > sizeof (c2s->key) || length > sizeof (s2c->key)) { DEBUG_LOG("Invalid algorithm"); return 0; } if (gnutls_prf_rfc5705(inst->tls_session, sizeof (NKE_EXPORTER_LABEL) - 1, NKE_EXPORTER_LABEL, sizeof (NKE_EXPORTER_CONTEXT_C2S) - 1, NKE_EXPORTER_CONTEXT_C2S, length, (char *)c2s->key) < 0 || gnutls_prf_rfc5705(inst->tls_session, sizeof (NKE_EXPORTER_LABEL) - 1, NKE_EXPORTER_LABEL, sizeof (NKE_EXPORTER_CONTEXT_S2C) - 1, NKE_EXPORTER_CONTEXT_S2C, length, (char *)s2c->key) < 0) { DEBUG_LOG("Could not export key"); return 0; } c2s->length = length; s2c->length = length; return 1; } /* ================================================== */ int NKSN_IsStopped(NKSN_Instance inst) { return inst->state == KE_STOPPED; } /* ================================================== */ void NKSN_StopSession(NKSN_Instance inst) { stop_session(inst); } /* ================================================== */ int NKSN_GetRetryFactor(NKSN_Instance inst) { return inst->retry_factor; }