/* * coreSNTP v1.2.0 * Copyright (C) 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * SPDX-License-Identifier: MIT * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** * @file core_sntp_client.c * @brief Implementation of the client API of the coreSNTP library. */ /* Standard includes. */ #include #include /* SNTP client library API include. */ #include "core_sntp_client.h" #include "core_sntp_config_defaults.h" /** * @brief Utility to convert fractions part of SNTP timestamp to milliseconds. * * @param[in] fractions The fractions value in an SNTP timestamp. */ #define FRACTIONS_TO_MS( fractions ) \ ( fractions / ( SNTP_FRACTION_VALUE_PER_MICROSECOND * 1000U ) ) SntpStatus_t Sntp_Init( SntpContext_t * pContext, const SntpServerInfo_t * pTimeServers, size_t numOfServers, uint32_t serverResponseTimeoutMs, uint8_t * pNetworkBuffer, size_t bufferSize, SntpResolveDns_t resolveDnsFunc, SntpGetTime_t getSystemTimeFunc, SntpSetTime_t setSystemTimeFunc, const UdpTransportInterface_t * pTransportIntf, const SntpAuthenticationInterface_t * pAuthIntf ) { SntpStatus_t status = SntpSuccess; /* Validate pointer parameters are not NULL. */ if( ( pContext == NULL ) || ( pTimeServers == NULL ) || ( pNetworkBuffer == NULL ) || ( resolveDnsFunc == NULL ) || ( getSystemTimeFunc == NULL ) || ( setSystemTimeFunc == NULL ) || ( pTransportIntf == NULL ) ) { LogError( ( "Invalid parameter: Pointer parameters (except pAuthIntf) cannot be NULL" ) ); status = SntpErrorBadParameter; } /* Validate the length of the servers list.*/ else if( numOfServers == 0U ) { LogError( ( "Invalid parameter: Size of server list cannot be zero" ) ); status = SntpErrorBadParameter; } /* Validate that the UDP transport interface functions are valid. */ else if( ( pTransportIntf->recvFrom == NULL ) || ( pTransportIntf->sendTo == NULL ) ) { LogError( ( "Invalid parameter: Function members of UDP transport interface cannot be NULL" ) ); status = SntpErrorBadParameter; } /* If an authentication interface is provided, validate that its function pointer * members are valid. */ else if( ( pAuthIntf != NULL ) && ( ( pAuthIntf->generateClientAuth == NULL ) || ( pAuthIntf->validateServerAuth == NULL ) ) ) { LogError( ( "Invalid parameter: Function members of authentication interface cannot be NULL" ) ); status = SntpErrorBadParameter; } else if( bufferSize < SNTP_PACKET_BASE_SIZE ) { LogError( ( "Cannot initialize context: Passed network buffer size is less than %u bytes: " "bufferSize=%lu", SNTP_PACKET_BASE_SIZE, ( unsigned long ) bufferSize ) ); status = SntpErrorBufferTooSmall; } else { /* Reset the context memory to zero. */ ( void ) memset( pContext, 0, sizeof( SntpContext_t ) ); /* Set the members of the context with passed parameters. */ pContext->pTimeServers = pTimeServers; pContext->numOfServers = numOfServers; pContext->responseTimeoutMs = serverResponseTimeoutMs; pContext->pNetworkBuffer = pNetworkBuffer; pContext->bufferSize = bufferSize; pContext->resolveDnsFunc = resolveDnsFunc; pContext->getTimeFunc = getSystemTimeFunc; pContext->setTimeFunc = setSystemTimeFunc; /* Copy contents of UDP transport interface to context. */ ( void ) memcpy( &pContext->networkIntf, pTransportIntf, sizeof( UdpTransportInterface_t ) ); /* If authentication interface has been passed, copy its contents to the context. */ if( pAuthIntf != NULL ) { ( void ) memcpy( &pContext->authIntf, pAuthIntf, sizeof( SntpAuthenticationInterface_t ) ); } /* Initialize the packet size member to the standard minimum SNTP packet size.*/ pContext->sntpPacketSize = SNTP_PACKET_BASE_SIZE; } return status; } /** * @brief Utility to calculate the difference in milliseconds between 2 * SNTP timestamps. * * @param[in] pCurrentTime The more recent timestamp. * @param[in] pOlderTime The older timestamp. * * @note This functions supports the edge case of SNTP timestamp overflow * when @p pCurrentTime represents time in NTP era 1 (i.e. time since 7 Feb 2036) * and the @p OlderTime represents time in NTP era 0 (i.e. time since 1st Jan 1900). * * @return Returns the calculated time duration between the two timestamps. * * @note This function returns the calculated time difference as unsigned 64 bit * to avoid integer overflow when converting time difference between the seconds part * of the timestamps, which are 32 bits wide, to milliseconds. */ static uint64_t calculateElapsedTimeMs( const SntpTimestamp_t * pCurrentTime, const SntpTimestamp_t * pOlderTime ) { uint64_t timeDiffMs = 0UL; uint32_t timeDiffSec = 0U; assert( pCurrentTime != NULL ); assert( pOlderTime != NULL ); /* Detect if SNTP time has overflown between the 2 timestamps. */ if( pCurrentTime->seconds < pOlderTime->seconds ) { /* Handle the SNTP time overflow by calculating the actual time * duration from pOlderTime, that exists in NTP era 0, to pCurrentTime, * that exists in NTP era 1. */ timeDiffSec = ( UINT32_MAX - pOlderTime->seconds ) + /* Time in NTP era 0. */ 1U + /* Epoch time in NTP era 1, i.e. 7 Feb 2036 6h:14m:28s. */ pCurrentTime->seconds; /* Time in NTP era 1. */ timeDiffMs = ( uint64_t ) timeDiffSec * 1000UL; } else { timeDiffSec = ( pCurrentTime->seconds - pOlderTime->seconds ); timeDiffMs = ( uint64_t ) timeDiffSec * 1000UL; } if( pCurrentTime->fractions > pOlderTime->fractions ) { timeDiffMs += ( ( uint64_t ) pCurrentTime->fractions - ( uint64_t ) pOlderTime->fractions ) / ( SNTP_FRACTION_VALUE_PER_MICROSECOND * 1000UL ); } else { timeDiffMs -= ( ( uint64_t ) pOlderTime->fractions - ( uint64_t ) pCurrentTime->fractions ) / ( SNTP_FRACTION_VALUE_PER_MICROSECOND * 1000UL ); } return timeDiffMs; } /** * @brief Validates the content of the SNTP context passed to the APIs to * check whether it represents an initialized context. * * @param[in] pContext The SNTP context to validate. * * @return Returns one of the following: * - #SntpSuccess if the context is verified to be initialized. * - #SntpErrorBadParameter if the context is NULL. * - #SntpErrorContextNotInitialized if the context is validated to be initialized. */ static SntpStatus_t validateContext( const SntpContext_t * pContext ) { SntpStatus_t status = SntpSuccess; /* Check if the context parameter is invalid. */ if( pContext == NULL ) { status = SntpErrorBadParameter; LogError( ( "Invalid context parameter: Context is NULL" ) ); } /* Validate pointer parameters are not NULL. */ else if( ( pContext->pTimeServers == NULL ) || ( pContext->pNetworkBuffer == NULL ) || ( pContext->resolveDnsFunc == NULL ) || ( pContext->getTimeFunc == NULL ) || ( pContext->setTimeFunc == NULL ) ) { status = SntpErrorContextNotInitialized; } /* Validate the size of the configured servers list, network buffer size and the state * variable for the SNTP packet size.*/ else if( ( pContext->numOfServers == 0U ) || ( pContext->bufferSize < SNTP_PACKET_BASE_SIZE ) || ( pContext->sntpPacketSize < SNTP_PACKET_BASE_SIZE ) ) { status = SntpErrorContextNotInitialized; } /* Validate that the UDP transport interface functions are valid. */ else if( ( pContext->networkIntf.recvFrom == NULL ) || ( pContext->networkIntf.sendTo == NULL ) ) { status = SntpErrorContextNotInitialized; } /* If an authentication interface is provided, validate that both its function pointer * members are valid. */ else if( ( ( pContext->authIntf.generateClientAuth != NULL ) && ( pContext->authIntf.validateServerAuth == NULL ) ) || ( ( pContext->authIntf.generateClientAuth == NULL ) && ( pContext->authIntf.validateServerAuth != NULL ) ) ) { status = SntpErrorContextNotInitialized; } else { status = SntpSuccess; } if( status == SntpErrorContextNotInitialized ) { LogError( ( "Invalid context parameter: Context is not initialized with Sntp_Init" ) ); } return status; } /** * @brief Sends SNTP request packet to the passed server over the network * using transport interface's send function. * * @note For the case of zero byte transmissions over the network, this function * repeatedly retries the send operation by calling the transport interface * until either: * 1. The requested number of bytes @p packetSize have been sent. * OR * 2. There is an error in sending data over the network. * * @note This function treats partial data transmissions as error as UDP * transport protocol does not support partial sends. * * @param[in] pNetworkIntf The UDP transport interface to use for * sending data over the network. * @param[in] timeServer The IPv4 address of the server to send the * SNTP request packet to. * @param[in] serverPort The port of the @p timeServer to send the * request to. * @param[in] getTimeFunc The function to query system time for * tracking retry time period of no data transmissions. * @param[in] pPacket The buffer containing the SNTP packet data * to send over the network. * @param[in] packetSize The size of data in the SNTP request packet. * @param[in] timeoutMs The timeout period for retry attempts of sending * SNTP request packet over the network. * * @return Returns #SntpSuccess on successful transmission of the entire * SNTP request packet over the network; #SntpErrorNetworkFailure * to indicate failure from transport interface; #SntpErrorSendTimeout if * time request could not be sent over the network within the @p timeoutMs * duration. */ static SntpStatus_t sendSntpPacket( const UdpTransportInterface_t * pNetworkIntf, uint32_t timeServer, uint16_t serverPort, SntpGetTime_t getTimeFunc, const uint8_t * pPacket, uint16_t packetSize, uint32_t timeoutMs ) { const uint8_t * pIndex = pPacket; int32_t bytesSent = 0; SntpTimestamp_t lastSendTime; bool shouldRetry = false; SntpStatus_t status = SntpErrorSendTimeout; assert( pPacket != NULL ); assert( getTimeFunc != NULL ); assert( pNetworkIntf != NULL ); assert( packetSize >= SNTP_PACKET_BASE_SIZE ); /* Record the starting time of attempting to send data. This begins the retry timeout * window. */ getTimeFunc( &lastSendTime ); /* Loop until the entire packet is sent. */ do { /* Reset flag for retrying send operation for the iteration. If request packet cannot be * sent and timeout has not occurred, the flag will be set later for the next iteration. */ shouldRetry = false; bytesSent = pNetworkIntf->sendTo( pNetworkIntf->pUserContext, timeServer, serverPort, pIndex, packetSize ); if( bytesSent < 0 ) { LogError( ( "Unable to send request packet: Transport send failed. " "ErrorCode=%ld.", ( long int ) bytesSent ) ); status = SntpErrorNetworkFailure; } else if( bytesSent == 0 ) { /* No bytes were sent over the network. Retry send if we have not timed out. */ SntpTimestamp_t currentTime; uint64_t elapsedTimeMs; getTimeFunc( ¤tTime ); /* Calculate time elapsed since last data was sent over network. */ elapsedTimeMs = calculateElapsedTimeMs( ¤tTime, &lastSendTime ); /* Check for timeout if we have been waiting to send any data over the network. */ if( elapsedTimeMs >= timeoutMs ) { LogError( ( "Unable to send request packet: Timed out retrying send: " "SendRetryTimeout=%ums", timeoutMs ) ); status = SntpErrorSendTimeout; } else { shouldRetry = true; } } /* Partial sends are not supported by UDP, which only supports sending the entire datagram as a whole. * Thus, if the transport send function returns status representing partial send, it will be treated as failure. */ else if( bytesSent != ( int32_t ) packetSize ) { LogError( ( "Unable to send request packet: Transport send returned unexpected bytes sent. " "ReturnCode=%ld, ExpectedCode=%u", ( long int ) bytesSent, packetSize ) ); status = SntpErrorNetworkFailure; } else { /* The time request packet has been sent over the network. */ status = SntpSuccess; } } while( shouldRetry == true ); return status; } /** * @brief Adds client authentication data to SNTP request packet by calling the * authentication interface. * * @param[in] pContext The SNTP context. * * @return Returns one of the following: * - #SntpSuccess if the interface function successfully appends client * authentication data. * - #SntpErrorAuthFailure when the interface returns either an error OR an * incorrect size of the client authentication data. */ static SntpStatus_t addClientAuthentication( SntpContext_t * pContext ) { SntpStatus_t status = SntpSuccess; uint16_t authDataSize = 0U; assert( pContext != NULL ); assert( pContext->authIntf.generateClientAuth != NULL ); assert( pContext->currentServerIndex <= pContext->numOfServers ); status = pContext->authIntf.generateClientAuth( pContext->authIntf.pAuthContext, &pContext->pTimeServers[ pContext->currentServerIndex ], pContext->pNetworkBuffer, pContext->bufferSize, &authDataSize ); if( status != SntpSuccess ) { LogError( ( "Unable to send time request: Client authentication function failed: " "RetStatus=%s", Sntp_StatusToStr( status ) ) ); } /* Sanity check that the returned authentication data size fits in the remaining space * of the request buffer besides the first #SNTP_PACKET_BASE_SIZE bytes. */ else if( authDataSize > ( pContext->bufferSize - SNTP_PACKET_BASE_SIZE ) ) { LogError( ( "Unable to send time request: Invalid authentication code size: " "AuthCodeSize=%lu, NetworkBufferSize=%lu", ( unsigned long ) authDataSize, ( unsigned long ) pContext->bufferSize ) ); status = SntpErrorAuthFailure; } else { /* With the authentication data added. calculate total SNTP request packet size. The same * size would be expected in the SNTP response from server. */ pContext->sntpPacketSize = SNTP_PACKET_BASE_SIZE + authDataSize; LogInfo( ( "Appended client authentication code to SNTP request packet:" " AuthCodeSize=%lu, TotalPacketSize=%lu", ( unsigned long ) authDataSize, ( unsigned long ) pContext->sntpPacketSize ) ); } return status; } SntpStatus_t Sntp_SendTimeRequest( SntpContext_t * pContext, uint32_t randomNumber, uint32_t blockTimeMs ) { SntpStatus_t status = SntpSuccess; /* Validate the context parameter. */ status = validateContext( pContext ); if( status == SntpSuccess ) { const SntpServerInfo_t * pServer = NULL; /* Set local variable for the currently indexed server to use for time * query. */ pServer = &pContext->pTimeServers[ pContext->currentServerIndex ]; LogDebug( ( "Using server %.*s for time query", ( int ) pServer->serverNameLen, pServer->pServerName ) ); /* Perform DNS resolution of the currently indexed server in the list * of configured servers. */ if( pContext->resolveDnsFunc( pServer, &pContext->currentServerAddr ) == false ) { LogError( ( "Unable to send time request: DNS resolution failed: Server=%.*s", ( int ) pServer->serverNameLen, pServer->pServerName ) ); status = SntpErrorDnsFailure; } else { LogDebug( ( "Server DNS resolved: Address=0x%08X", pContext->currentServerAddr ) ); } if( status == SntpSuccess ) { /* Obtain current system time to generate SNTP request packet. */ pContext->getTimeFunc( &pContext->lastRequestTime ); LogDebug( ( "Obtained current time for SNTP request packet: Time=%us %ums", pContext->lastRequestTime.seconds, FRACTIONS_TO_MS( pContext->lastRequestTime.fractions ) ) ); /* Generate SNTP request packet with the current system time and * the passed random number. */ status = Sntp_SerializeRequest( &pContext->lastRequestTime, randomNumber, pContext->pNetworkBuffer, pContext->bufferSize ); /* The serialization should be successful as all parameter validation has * been done before. */ assert( status == SntpSuccess ); } /* If an authentication interface has been configured, call the function to append client * authentication data to SNTP request buffer. */ if( ( status == SntpSuccess ) && ( pContext->authIntf.generateClientAuth != NULL ) ) { status = addClientAuthentication( pContext ); } if( status == SntpSuccess ) { LogInfo( ( "Sending serialized SNTP request packet to the server: Addr=%u, Port=%u", pContext->currentServerAddr, pContext->pTimeServers[ pContext->currentServerIndex ].port ) ); /* Send the request packet over the network to the time server. */ status = sendSntpPacket( &pContext->networkIntf, pContext->currentServerAddr, pContext->pTimeServers[ pContext->currentServerIndex ].port, pContext->getTimeFunc, pContext->pNetworkBuffer, pContext->sntpPacketSize, blockTimeMs ); } } return status; } /** * @brief Utility to update the SNTP context to rotate the server of use for subsequent * time request(s). * * @note If there is no next server remaining, after the current server's index, in the list of * configured servers, the server rotation algorithm wraps around to the first server in the list. * The wrap around is done so that an application using the library for a long-running SNTP client * functionality (like a daemon task) does not become dysfunctional after all configured time * servers have been used up. Time synchronization can be a critical functionality for a system * and the wrap around logic ensures that the SNTP client continues to function in such a case. * * @note Server rotation is performed ONLY when either of: * - The current server responds with a rejection for time request. * OR * - The current server response wait has timed out. */ static void rotateServerForNextTimeQuery( SntpContext_t * pContext ) { size_t nextServerIndex = ( pContext->currentServerIndex + 1U ) % pContext->numOfServers; LogInfo( ( "Rotating server for next time query: PreviousServer=%.*s, NextServer=%.*s", ( int ) pContext->pTimeServers[ pContext->currentServerIndex ].serverNameLen, pContext->pTimeServers[ pContext->currentServerIndex ].pServerName, ( int ) pContext->pTimeServers[ nextServerIndex ].serverNameLen, pContext->pTimeServers[ nextServerIndex ].pServerName ) ); pContext->currentServerIndex = nextServerIndex; } /** * @brief This function attempts to receive the SNTP response packet from a server. * * @note This function treats reads of data sizes less than the expected server response packet, * as an error as UDP does not support partial reads. Such a scenario can exist either due: * - An error in the server sending its response with smaller packet size than the request packet OR * - A malicious attacker spoofing or modifying server response OR * - An error in the UDP transport interface implementation for read operation. * * @param[in] pTransportIntf The UDP transport interface to use for receiving data from * the network. * @param[in] timeServer The server to read the response from the network. * @param[in] serverPort The port of the server to read the response from. * @param[in, out] pBuffer This will be filled with the server response read from the * network. * @param[in] responseSize The size of server response to read from the network. * * @return It returns one of the following: * - #SntpSuccess if an SNTP response packet is received from the network. * - #SntpNoResponseReceived if a server response is not received from the network. * - #SntpErrorNetworkFailure if there is an internal failure in reading from the network * in the user-defined transport interface. */ static SntpStatus_t receiveSntpResponse( const UdpTransportInterface_t * pTransportIntf, uint32_t timeServer, uint16_t serverPort, uint8_t * pBuffer, uint16_t responseSize ) { SntpStatus_t status = SntpNoResponseReceived; int32_t bytesRead = 0; assert( pTransportIntf != NULL ); assert( pTransportIntf->recvFrom != NULL ); assert( pBuffer != NULL ); assert( responseSize >= SNTP_PACKET_BASE_SIZE ); bytesRead = pTransportIntf->recvFrom( pTransportIntf->pUserContext, timeServer, serverPort, pBuffer, responseSize ); /* Negative return code indicates error. */ if( bytesRead < 0 ) { status = SntpErrorNetworkFailure; LogError( ( "Unable to receive server response: Transport receive failed: Code=%ld", ( long int ) bytesRead ) ); } /* If the packet was not available on the network, check whether we can retry. */ else if( bytesRead == 0 ) { status = SntpNoResponseReceived; } /* Partial reads are not supported by UDP, which only supports receiving the entire datagram as a whole. * Thus, if the transport receive function returns reception of partial data, it will be treated as failure. */ else if( bytesRead != ( int32_t ) responseSize ) { LogError( ( "Failed to receive server response: Transport recv returned less than expected bytes." "ExpectedBytes=%u, ReadBytes=%ld", responseSize, ( long int ) bytesRead ) ); status = SntpErrorNetworkFailure; } else { LogDebug( ( "Received server response: PacketSize=%ld", ( long int ) bytesRead ) ); status = SntpSuccess; } return status; } /** * @brief Processes the response from a server by de-serializing the SNTP packet to * validate the server (if an authentication interface has been configured), determine * whether server has accepted or rejected the time request, and update the system clock * if the server responded positively with time. * * @param[in] pContext The SNTP context representing the SNTP client. * @param[in] pResponseRxTime The time of receiving the server response from the network. * * @return It returns one of the following: * - #SntpSuccess if the server response is successfully de-serialized and system clock * updated. * - #SntpErrorAuthFailure if there is internal failure in user-defined authentication * interface when validating server from the response. * - #SntpServerNotAuthenticated if the server failed authenticated check in the user-defined * interface. * - #SntpRejectedResponse if the server has rejected the time request in its response. * - #SntpInvalidResponse if the server response failed sanity checks. */ static SntpStatus_t processServerResponse( SntpContext_t * pContext, const SntpTimestamp_t * pResponseRxTime ) { SntpStatus_t status = SntpSuccess; const SntpServerInfo_t * pServer = &pContext->pTimeServers[ pContext->currentServerIndex ]; assert( pContext != NULL ); assert( pResponseRxTime != NULL ); if( pContext->authIntf.validateServerAuth != NULL ) { /* Verify the server from the authentication data in the SNTP response packet. */ status = pContext->authIntf.validateServerAuth( pContext->authIntf.pAuthContext, pServer, pContext->pNetworkBuffer, pContext->sntpPacketSize ); assert( ( status == SntpSuccess ) || ( status == SntpErrorAuthFailure ) || ( status == SntpServerNotAuthenticated ) ); if( status != SntpSuccess ) { LogError( ( "Unable to use server response: Server authentication function failed: " "ReturnStatus=%s", Sntp_StatusToStr( status ) ) ); } else { LogDebug( ( "Server response has been validated: Server=%.*s", ( int ) pServer->serverNameLen, pServer->pServerName ) ); } } if( status == SntpSuccess ) { SntpResponseData_t parsedResponse; /* De-serialize response packet to determine whether the server accepted or rejected * the request for time. Also, calculate the system clock offset if the server responded * with time. */ status = Sntp_DeserializeResponse( &pContext->lastRequestTime, pResponseRxTime, pContext->pNetworkBuffer, pContext->sntpPacketSize, &parsedResponse ); /* We do not expect the following errors to be returned as the context * has been validated in the Sntp_ReceiveTimeResponse API. */ assert( status != SntpErrorBadParameter ); assert( status != SntpErrorBufferTooSmall ); if( ( status == SntpRejectedResponseChangeServer ) || ( status == SntpRejectedResponseRetryWithBackoff ) || ( status == SntpRejectedResponseOtherCode ) ) { /* Server has rejected the time request. Thus, we will rotate to the next time server * in the list. */ rotateServerForNextTimeQuery( pContext ); LogError( ( "Unable to use server response: Server has rejected request for time: RejectionCode=%.*s", ( int ) SNTP_KISS_OF_DEATH_CODE_LENGTH, ( char * ) &parsedResponse.rejectedResponseCode ) ); status = SntpRejectedResponse; } else if( status == SntpInvalidResponse ) { LogError( ( "Unable to use server response: Server response failed sanity checks." ) ); } else { /* Server has responded successfully with time, and we have calculated the clock offset * of system clock relative to the server.*/ LogDebug( ( "Updating system time: ServerTime=%u %ums ClockOffset=%lums", parsedResponse.serverTime.seconds, FRACTIONS_TO_MS( parsedResponse.serverTime.fractions ), parsedResponse.clockOffsetMs ) ); /* Update the system clock with the calculated offset. */ pContext->setTimeFunc( pServer, &parsedResponse.serverTime, parsedResponse.clockOffsetMs, parsedResponse.leapSecondType ); status = SntpSuccess; } } /* Reset the last request time state in context to protect against replay attacks. * Note: The last request time is not cleared when a rejection response packet is received and the client does * has not authenticated server from the response. This is because clearing of the state causes the coreSNTP * library to discard any subsequent server response packets (as the "originate timestamp" of those packets will * not match the last request time value of the context), and thus, an attacker can cause Denial of Service * attacks by spoofing server response before the actual server is able to respond. */ if( ( status == SntpSuccess ) || ( ( pContext->authIntf.validateServerAuth != NULL ) && ( status == SntpRejectedResponse ) ) ) { /* In the attack of SNTP request packet being replayed, the replayed request packet is serviced by * SNTP/NTP server with SNTP response (as servers are stateless) and client receives the response * containing new values of server timestamps but the stale value of "originate timestamp". * To prevent the coreSNTP library from servicing such a server response (associated with the replayed * SNTP request packet), the last request timestamp state is cleared in the context after receiving the * first valid server response. Therefore, any subsequent server response(s) from replayed client request * packets can be invalidated due to the "originate timestamp" not matching the last request time stored * in the context. * Note: If an attacker spoofs a server response with a zero "originate timestamp" after the coreSNTP * library (i.e. the SNTP client) has cleared the internal state to zero, the spoofed packet will be * discarded as the coreSNTP serializer does not accept server responses with zero value for timestamps. */ pContext->lastRequestTime.seconds = 0U; pContext->lastRequestTime.fractions = 0U; } return status; } /** * @brief Determines whether a retry attempt should be made to receive server response packet from the network * depending on the timing constraints of server response timeout, @p responseTimeoutMs, and the block time * period, @p blockTimeMs, passed. If neither of the time windows have expired, the function determines that the * read operation can be re-tried. * * @param[in] pCurrentTime The current time in the system used for calculating elapsed time windows. * @param[in] pReadStartTime The time of the first read attempt in the current set of read tries occurring * from the Sntp_ReceiveTimeRequest API call by the application. This time is used for calculating the elapsed * time to determine whether the block time has expired. * @param[in] pRequestTime The time of sending the SNTP request to the server for which the response is * awaited. This time is used for calculating total elapsed elapsed time of waiting for server response to * determine if a server response timeout has occurred. * @param[in] responseTimeoutMs The server response timeout configuration. * @param[in] blockTimeMs The maximum block time of waiting for server response across read tries in the current * call made by application to Sntp_ReceiveTimeResponse API. * @param[out] pHasResponseTimedOut This will be populated with state to indicate whether the wait for server * response has timed out. * * @return Returns true for retrying read operation of server response; false on either server response timeout * OR completion of block time window. */ static bool decideAboutReadRetry( const SntpTimestamp_t * pCurrentTime, const SntpTimestamp_t * pReadStartTime, const SntpTimestamp_t * pRequestTime, uint32_t responseTimeoutMs, uint32_t blockTimeMs, bool * pHasResponseTimedOut ) { uint64_t timeSinceRequestMs = 0UL; uint64_t timeElapsedInReadAttempts = 0UL; bool shouldRetry = false; assert( pCurrentTime != NULL ); assert( pReadStartTime != NULL ); assert( pRequestTime != NULL ); assert( pHasResponseTimedOut != NULL ); /* Calculate time elapsed since the time request was sent to the server * to determine whether the server response has timed out. */ timeSinceRequestMs = calculateElapsedTimeMs( pCurrentTime, pRequestTime ); /* Calculate the time elapsed across all the read attempts so far to determine * whether the block time window for reading server response has expired. */ timeElapsedInReadAttempts = calculateElapsedTimeMs( pCurrentTime, pReadStartTime ); /* Check whether a response timeout has occurred to inform whether we should * wait for server response anymore. */ if( timeSinceRequestMs >= ( uint64_t ) responseTimeoutMs ) { shouldRetry = false; *pHasResponseTimedOut = true; LogError( ( "Unable to receive response: Server response has timed out: " "RequestTime=%us %ums, TimeoutDuration=%ums, ElapsedTime=%lu", pRequestTime->seconds, FRACTIONS_TO_MS( pRequestTime->fractions ), responseTimeoutMs, timeSinceRequestMs ) ); } /* Check whether the block time window has expired to determine whether read can be retried. */ else if( timeElapsedInReadAttempts >= ( uint64_t ) blockTimeMs ) { shouldRetry = false; LogDebug( ( "Did not receive server response: Read block time has expired: " "BlockTime=%ums, ResponseWaitElapsedTime=%lums", blockTimeMs, timeSinceRequestMs ) ); } else { shouldRetry = true; LogDebug( ( "Did not receive server response: Retrying read: " "BlockTime=%ums, ResponseWaitElapsedTime=%lums, ResponseTimeout=%u", blockTimeMs, timeSinceRequestMs, responseTimeoutMs ) ); } return shouldRetry; } SntpStatus_t Sntp_ReceiveTimeResponse( SntpContext_t * pContext, uint32_t blockTimeMs ) { SntpStatus_t status = SntpNoResponseReceived; bool hasResponseTimedOut = false; /* Validate the context parameter. */ status = validateContext( pContext ); if( status == SntpSuccess ) { SntpTimestamp_t startTime, currentTime; const SntpTimestamp_t * pRequestTime = &pContext->lastRequestTime; bool shouldRetry = false; /* Record time before read attempts so that it can be used as base time for * for tracking the block time window across read retries. */ pContext->getTimeFunc( &startTime ); do { /* Reset the retry read operation flag. If the server response is not received in the current iteration's read * attempt and the wait has not timed out, the flag will be set to perform a retry. */ shouldRetry = false; /* Make an attempt to read the server response from the network. */ status = receiveSntpResponse( &pContext->networkIntf, pContext->currentServerAddr, pContext->pTimeServers[ pContext->currentServerIndex ].port, pContext->pNetworkBuffer, pContext->sntpPacketSize ); /* If the server response is received, deserialize it, validate the server * (if authentication interface is provided), and update system time with * the calculated clock offset. */ if( status == SntpSuccess ) { /* Get current time to de-serialize the receive server response packet. */ pContext->getTimeFunc( ¤tTime ); status = processServerResponse( pContext, ¤tTime ); } else if( status == SntpNoResponseReceived ) { /* Get current time to determine whether another attempt for reading the packet can * be made. */ pContext->getTimeFunc( ¤tTime ); /* Set the flag to retry read of server response from the network. */ shouldRetry = decideAboutReadRetry( ¤tTime, &startTime, pRequestTime, pContext->responseTimeoutMs, blockTimeMs, &hasResponseTimedOut ); } else { /* Empty else marker. */ } } while( shouldRetry == true ); /* If the wait for server response to the time request has timed out, rotate the server of use in the * context for subsequent time request(s). Also, update the return status to indicate response timeout. */ if( hasResponseTimedOut == true ) { status = SntpErrorResponseTimeout; /* Rotate server to the next in the list of configured servers in the context. */ rotateServerForNextTimeQuery( pContext ); } } return status; } const char * Sntp_StatusToStr( SntpStatus_t status ) { const char * pString = NULL; switch( status ) { case SntpSuccess: pString = "SntpSuccess"; break; case SntpErrorBadParameter: pString = "SntpErrorBadParameter"; break; case SntpRejectedResponseChangeServer: pString = "SntpRejectedResponseChangeServer"; break; case SntpRejectedResponseRetryWithBackoff: pString = "SntpRejectedResponseRetryWithBackoff"; break; case SntpRejectedResponseOtherCode: pString = "SntpRejectedResponseOtherCode"; break; case SntpErrorBufferTooSmall: pString = "SntpErrorBufferTooSmall"; break; case SntpInvalidResponse: pString = "SntpInvalidResponse"; break; case SntpZeroPollInterval: pString = "SntpZeroPollInterval"; break; case SntpErrorTimeNotSupported: pString = "SntpErrorTimeNotSupported"; break; case SntpErrorDnsFailure: pString = "SntpErrorDnsFailure"; break; case SntpErrorNetworkFailure: pString = "SntpErrorNetworkFailure"; break; case SntpServerNotAuthenticated: pString = "SntpServerNotAuthenticated"; break; case SntpErrorAuthFailure: pString = "SntpErrorAuthFailure"; break; default: pString = "Invalid status code!"; break; } return pString; }