diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 335bd032..0d1aedad 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -89,7 +89,7 @@ jobs: env: AUTH_KEY: ${{ secrets.AUTH_KEY }} run: | - echo "#define AUTH_DEFAULT_KEY \"$AUTH_KEY\"" > PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h + echo "#define AUTH_DEFAULT_KEY \"$AUTH_KEY\"" > PROVESFlightControllerReference/Components/TcSecurityDecryptor/AuthDefaultKey.h - name: Build MCUBoot run: | @@ -158,7 +158,7 @@ jobs: env: AUTH_KEY: ${{ secrets.AUTH_KEY }} run: | - echo "#define AUTH_DEFAULT_KEY \"$AUTH_KEY\"" > PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h + echo "#define AUTH_DEFAULT_KEY \"$AUTH_KEY\"" > PROVESFlightControllerReference/Components/TcSecurityDecryptor/AuthDefaultKey.h - name: Install Framer Plugin run: | @@ -417,7 +417,7 @@ jobs: env: AUTH_KEY: ${{ secrets.AUTH_KEY }} run: | - echo "#define AUTH_DEFAULT_KEY \"$AUTH_KEY\"" > PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h + echo "#define AUTH_DEFAULT_KEY \"$AUTH_KEY\"" > PROVESFlightControllerReference/Components/TcSecurityDecryptor/AuthDefaultKey.h - name: Install Framer Plugin run: | diff --git a/AGENTS.md b/AGENTS.md index a6e88670..dcd2dc60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -581,7 +581,7 @@ After compiling, upload the sequence through GDS for execution on the board. ### Authentication & Security -Commands can be HMAC-authenticated using the `TcSecurityDeframer` component. The authentication key is stored in `PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h`. +Commands can be HMAC-authenticated using the `TcSecurityDecryptor` component. The authentication key is stored in `PROVESFlightControllerReference/Components/TcSecurityDecryptor/AuthDefaultKey.h`. ```bash make generate-auth-key # Generate a new random HMAC key (only if file doesn't exist) diff --git a/Framing/src/authenticate_plugin.py b/Framing/src/authenticate_plugin.py index 0b99fc9a..398d9eb0 100644 --- a/Framing/src/authenticate_plugin.py +++ b/Framing/src/authenticate_plugin.py @@ -31,9 +31,7 @@ def get_default_auth_key_from_header() -> str: ValueError: If AuthDefaultKey.h does not contain a valid key IOError: If there is an error reading the file """ - path = ( - "PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h" - ) + path = "PROVESFlightControllerReference/Components/TcSecurityDecryptor/AuthDefaultKey.h" if not os.path.exists(path): raise FileNotFoundError( diff --git a/Makefile b/Makefile index af982516..a6dccbf6 100644 --- a/Makefile +++ b/Makefile @@ -116,7 +116,7 @@ docs-sync: ## Sync SDD files from components to docs-site @cp PROVESFlightControllerReference/Components/FsSpace/docs/sdd.md docs-site/components/FsSpace.md @cp PROVESFlightControllerReference/Components/NullPrmDb/docs/sdd.md docs-site/components/NullPrmDb.md @# Copy Security Components - @cp PROVESFlightControllerReference/Components/TcSecurityDeframer/docs/sdd.md docs-site/components/TcSecurityDeframer.md + @cp PROVESFlightControllerReference/Components/TcSecurityDecryptor/docs/sdd.md docs-site/components/TcSecurityDecryptor.md @cp PROVESFlightControllerReference/Components/ProvesRouter/docs/sdd.md docs-site/components/ProvesRouter.md @# Copy images @find PROVESFlightControllerReference -path "*/docs/img/*" -type f -exec cp {} docs-site/components/img/ \; 2>/dev/null || true @@ -155,7 +155,7 @@ check-console-disabled: uv ## Fail if the Zephyr UART console is enabled (it cor ##@ Authentication Keys -AUTH_DEFAULT_KEY_HEADER ?= PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h +AUTH_DEFAULT_KEY_HEADER ?= PROVESFlightControllerReference/Components/TcSecurityDecryptor/AuthDefaultKey.h AUTH_KEY_TEMPLATE ?= scripts/generate_auth_default_key.h .PHONY: generate-auth-key @@ -466,7 +466,7 @@ copy-secrets: @mkdir -p ./keys/ @cp $(SECRETS_DIR)/proves.pem ./keys/ @cp $(SECRETS_DIR)/proves.pub.pem ./keys/ - @cp $(SECRETS_DIR)/AuthDefaultKey.h ./PROVESFlightControllerReference/Components/TcSecurityDeframer/ + @cp $(SECRETS_DIR)/AuthDefaultKey.h ./PROVESFlightControllerReference/Components/TcSecurityDecryptor/ @echo "Copied secret files ๐Ÿคซ" .PHONY: make-ci-spacecraft-id diff --git a/PROVESFlightControllerReference/ComCcsdsLora/ComCcsds.fpp b/PROVESFlightControllerReference/ComCcsdsLora/ComCcsds.fpp index a7c0e775..e65a4bdb 100644 --- a/PROVESFlightControllerReference/ComCcsdsLora/ComCcsds.fpp +++ b/PROVESFlightControllerReference/ComCcsdsLora/ComCcsds.fpp @@ -98,15 +98,17 @@ module ComCcsdsLora { instance apidManager: Svc.Ccsds.ApidManager base id ComCcsdsConfig.BASE_ID_LORA + 0x09000 - instance tcSecurityDeframer: Components.TcSecurityDeframer base id ComCcsdsConfig.BASE_ID_LORA + 0x0B000 \ + instance tcSecurityDecryptor: Components.TcSecurityDecryptor base id ComCcsdsConfig.BASE_ID_LORA + 0x0B000 \ { phase Fpp.ToCpp.Phases.startTasks """ // configure() reads parameters, so it must run after loadParameters(); // the startTasks phase is the first phase after parameters are loaded. - ComCcsdsLora::tcSecurityDeframer.configure(); + ComCcsdsLora::tcSecurityDecryptor.configure(); """ } + instance sdlsDeframer: Svc.Ccsds.CcsdsSdlsDeframer base id ComCcsdsConfig.BASE_ID_LORA + 0x0C000 + topology Subtopology { # Usage Note: # @@ -135,7 +137,8 @@ module ComCcsdsLora { instance spacePacketFramer instance apidManager instance aggregator - instance tcSecurityDeframer + instance tcSecurityDecryptor + instance sdlsDeframer connections Downlink { # ComQueue <-> SpacePacketFramer @@ -167,12 +170,15 @@ module ComCcsdsLora { # FrameAccumulator <-> TcDeframer frameAccumulator.dataOut -> tcDeframer.dataIn tcDeframer.dataReturnOut -> frameAccumulator.dataReturnIn - # TcSecurityDeframer <-> SpacePacketDeframer - tcSecurityDeframer.dataOut -> spacePacketDeframer.dataIn - spacePacketDeframer.dataReturnOut -> tcSecurityDeframer.dataReturnIn - # TcDeframer <-> TcSecurityDeframer - tcDeframer.dataOut -> tcSecurityDeframer.dataIn - tcSecurityDeframer.dataReturnOut -> tcDeframer.dataReturnIn + # TcDeframer <-> SdlsDeframer <-> SpacePacketDeframer + tcDeframer.dataOut -> sdlsDeframer.dataIn + sdlsDeframer.dataReturnOut -> tcDeframer.dataReturnIn + sdlsDeframer.dataOut -> spacePacketDeframer.dataIn + spacePacketDeframer.dataReturnOut -> sdlsDeframer.dataReturnIn + sdlsDeframer.decryptOut -> tcSecurityDecryptor.decryptIn + tcSecurityDecryptor.decryptOut -> sdlsDeframer.decryptIn + sdlsDeframer.decryptReturnOut -> tcSecurityDecryptor.decryptReturnIn + tcSecurityDecryptor.bufferReturnOut -> sdlsDeframer.bufferReturnIn # SpacePacketDeframer APID validation spacePacketDeframer.validateApidSeqCount -> apidManager.validateApidSeqCountIn # SpacePacketDeframer <-> ProvesRouter (routes both commands and files) diff --git a/PROVESFlightControllerReference/ComCcsdsSband/ComCcsds.fpp b/PROVESFlightControllerReference/ComCcsdsSband/ComCcsds.fpp index 0c75b19a..4424c824 100644 --- a/PROVESFlightControllerReference/ComCcsdsSband/ComCcsds.fpp +++ b/PROVESFlightControllerReference/ComCcsdsSband/ComCcsds.fpp @@ -98,15 +98,17 @@ module ComCcsdsSband { instance apidManager: Svc.Ccsds.ApidManager base id ComCcsdsConfig.BASE_ID_SBAND + 0x09000 - instance tcSecurityDeframer: Components.TcSecurityDeframer base id ComCcsdsConfig.BASE_ID_SBAND + 0x0B000 \ + instance tcSecurityDecryptor: Components.TcSecurityDecryptor base id ComCcsdsConfig.BASE_ID_SBAND + 0x0B000 \ { phase Fpp.ToCpp.Phases.startTasks """ // configure() reads parameters, so it must run after loadParameters(); // the startTasks phase is the first phase after parameters are loaded. - ComCcsdsSband::tcSecurityDeframer.configure(); + ComCcsdsSband::tcSecurityDecryptor.configure(); """ } + instance sdlsDeframer: Svc.Ccsds.CcsdsSdlsDeframer base id ComCcsdsConfig.BASE_ID_SBAND + 0x0C000 + topology Subtopology { # Usage Note: # @@ -135,7 +137,8 @@ module ComCcsdsSband { instance spacePacketFramer instance apidManager instance aggregator - instance tcSecurityDeframer + instance tcSecurityDecryptor + instance sdlsDeframer connections Downlink { # ComQueue <-> SpacePacketFramer @@ -167,12 +170,15 @@ module ComCcsdsSband { # FrameAccumulator <-> TcDeframer frameAccumulator.dataOut -> tcDeframer.dataIn tcDeframer.dataReturnOut -> frameAccumulator.dataReturnIn - # TcSecurityDeframer <-> SpacePacketDeframer - tcSecurityDeframer.dataOut -> spacePacketDeframer.dataIn - spacePacketDeframer.dataReturnOut -> tcSecurityDeframer.dataReturnIn - # TcDeframer <-> TcSecurityDeframer - tcDeframer.dataOut -> tcSecurityDeframer.dataIn - tcSecurityDeframer.dataReturnOut -> tcDeframer.dataReturnIn + # TcDeframer <-> SdlsDeframer <-> SpacePacketDeframer + tcDeframer.dataOut -> sdlsDeframer.dataIn + sdlsDeframer.dataReturnOut -> tcDeframer.dataReturnIn + sdlsDeframer.dataOut -> spacePacketDeframer.dataIn + spacePacketDeframer.dataReturnOut -> sdlsDeframer.dataReturnIn + sdlsDeframer.decryptOut -> tcSecurityDecryptor.decryptIn + tcSecurityDecryptor.decryptOut -> sdlsDeframer.decryptIn + sdlsDeframer.decryptReturnOut -> tcSecurityDecryptor.decryptReturnIn + tcSecurityDecryptor.bufferReturnOut -> sdlsDeframer.bufferReturnIn # SpacePacketDeframer APID validation spacePacketDeframer.validateApidSeqCount -> apidManager.validateApidSeqCountIn # SpacePacketDeframer <-> ProvesRouter (routes both commands and files) diff --git a/PROVESFlightControllerReference/ComCcsdsUart/ComCcsds.fpp b/PROVESFlightControllerReference/ComCcsdsUart/ComCcsds.fpp index 832dd0ce..9138c27c 100644 --- a/PROVESFlightControllerReference/ComCcsdsUart/ComCcsds.fpp +++ b/PROVESFlightControllerReference/ComCcsdsUart/ComCcsds.fpp @@ -110,15 +110,17 @@ module ComCcsdsUart { instance comStub: Svc.ComStub base id ComCcsdsConfig.BASE_ID_UART + 0x0A000 - instance tcSecurityDeframer: Components.TcSecurityDeframer base id ComCcsdsConfig.BASE_ID_UART + 0x0B000 \ + instance tcSecurityDecryptor: Components.TcSecurityDecryptor base id ComCcsdsConfig.BASE_ID_UART + 0x0B000 \ { phase Fpp.ToCpp.Phases.startTasks """ // configure() reads parameters, so it must run after loadParameters(); // the startTasks phase is the first phase after parameters are loaded. - ComCcsdsUart::tcSecurityDeframer.configure(); + ComCcsdsUart::tcSecurityDecryptor.configure(); """ } + instance sdlsDeframer: Svc.Ccsds.CcsdsSdlsDeframer base id ComCcsdsConfig.BASE_ID_UART + 0x0C000 + topology FramingSubtopology { # Usage Note: # @@ -147,7 +149,8 @@ module ComCcsdsUart { instance spacePacketFramer instance apidManager instance aggregator - instance tcSecurityDeframer + instance tcSecurityDecryptor + instance sdlsDeframer connections Downlink { # ComQueue <-> SpacePacketFramer @@ -181,13 +184,15 @@ module ComCcsdsUart { frameAccumulator.dataOut -> tcDeframer.dataIn tcDeframer.dataReturnOut -> frameAccumulator.dataReturnIn - # TcSecurityDeframer <-> SpacePacketDeframer - tcSecurityDeframer.dataOut -> spacePacketDeframer.dataIn - spacePacketDeframer.dataReturnOut -> tcSecurityDeframer.dataReturnIn - - # TcDeframer <-> TcSecurityDeframer - tcDeframer.dataOut -> tcSecurityDeframer.dataIn - tcSecurityDeframer.dataReturnOut -> tcDeframer.dataReturnIn + # TcDeframer <-> SdlsDeframer <-> SpacePacketDeframer + tcDeframer.dataOut -> sdlsDeframer.dataIn + sdlsDeframer.dataReturnOut -> tcDeframer.dataReturnIn + sdlsDeframer.dataOut -> spacePacketDeframer.dataIn + spacePacketDeframer.dataReturnOut -> sdlsDeframer.dataReturnIn + sdlsDeframer.decryptOut -> tcSecurityDecryptor.decryptIn + tcSecurityDecryptor.decryptOut -> sdlsDeframer.decryptIn + sdlsDeframer.decryptReturnOut -> tcSecurityDecryptor.decryptReturnIn + tcSecurityDecryptor.bufferReturnOut -> sdlsDeframer.bufferReturnIn # SpacePacketDeframer APID validation spacePacketDeframer.validateApidSeqCount -> apidManager.validateApidSeqCountIn diff --git a/PROVESFlightControllerReference/Components/CMakeLists.txt b/PROVESFlightControllerReference/Components/CMakeLists.txt index f81ec5c5..023581ac 100644 --- a/PROVESFlightControllerReference/Components/CMakeLists.txt +++ b/PROVESFlightControllerReference/Components/CMakeLists.txt @@ -23,6 +23,6 @@ add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PowerMonitor/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ResetManager/") #add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/SBand/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/StartupManager/") -add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/TcSecurityDeframer/") +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/TcSecurityDecryptor/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ThermalManager/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/Watchdog") diff --git a/PROVESFlightControllerReference/Components/ProvesRouter/Bypasser.cpp b/PROVESFlightControllerReference/Components/ProvesRouter/Bypasser.cpp index df10645f..9ae8c23f 100644 --- a/PROVESFlightControllerReference/Components/ProvesRouter/Bypasser.cpp +++ b/PROVESFlightControllerReference/Components/ProvesRouter/Bypasser.cpp @@ -41,9 +41,9 @@ FieldParseResult parseOpCode(const uint8_t* buffer, const size_t size) //! then convert the opcode field value from decimal to hexadecimal static constexpr uint32_t kBypassOpCodes[] = { 0x01000000, //!< CdhCore.cmdDisp.CMD_NO_OP - 0x2100B000, //!< ComCcsdsUart.tcSecurityDeframer.GET_SEQ_NUM - 0x2200B000, //!< ComCcsdsLora.tcSecurityDeframer.GET_SEQ_NUM - 0x2300B000, //!< ComCcsdsSband.tcSecurityDeframer.GET_SEQ_NUM + 0x2100B000, //!< ComCcsdsUart.tcSecurityDecryptor.GET_SEQ_NUM + 0x2200B000, //!< ComCcsdsLora.tcSecurityDecryptor.GET_SEQ_NUM + 0x2300B000, //!< ComCcsdsSband.tcSecurityDecryptor.GET_SEQ_NUM 0x10065000, //!< ReferenceDeployment.amateurRadio.TELL_JOKE }; diff --git a/PROVESFlightControllerReference/Components/ProvesRouter/docs/sdd.md b/PROVESFlightControllerReference/Components/ProvesRouter/docs/sdd.md index 68408392..8ba4446d 100644 --- a/PROVESFlightControllerReference/Components/ProvesRouter/docs/sdd.md +++ b/PROVESFlightControllerReference/Components/ProvesRouter/docs/sdd.md @@ -10,13 +10,13 @@ The `Svc::ProvesRouter` component supports `Fw::ComPacketType::FW_PACKET_COMMAND ## Security Policy Enforcement -`Svc::ProvesRouter` is the policy point of the uplink security design. Upstream, `Components::TcSecurityDeframer` verifies each frame (SPI, anti-replay sequence number, MAC) and records the result in the `ComCfg::FrameContext` `authenticated` flag without dropping anything. The router then enforces: +`Svc::ProvesRouter` is the policy point of the uplink security design. Upstream, `Components::TcSecurityDecryptor` (a decryptor behind the upstream `Svc::Ccsds::CcsdsSdlsDeframer`) verifies each frame (security association index, anti-replay sequence number, MAC) and records the result in the `ComCfg::FrameContext` `authenticated` flag without dropping anything. The router then enforces: - Packets with `authenticated == true` are routed normally. - Unauthenticated packets are routed only if their opcode is on the hardcoded bypass allowlist (`Components::PacketBypasser::bypassPacket` in `Bypasser.cpp`), which permits public commands such as `CMD_NO_OP`, `GET_SEQ_NUM`, and `TELL_JOKE`. - All other unauthenticated packets are rejected: ownership is returned via `dataReturnOut` and the packet is not routed. -Because bypassed packets never pass through the authenticated-accept path in TcSecurityDeframer, they cannot advance the anti-replay sequence number. +Because bypassed packets never pass through the authenticated-accept path in TcSecurityDecryptor, they cannot advance the anti-replay sequence number. About memory management, all buffers sent by `Svc::ProvesRouter` on the `fileOut` and `unknownDataOut` ports are expected to be returned to the router through the `fileBufferReturnIn` port for deallocation. diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.cpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.cpp similarity index 69% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.cpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.cpp index 5e760442..80d40047 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.cpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.cpp @@ -97,23 +97,44 @@ PacketAuthenticator::KeyImportResult importHmacKey(const char* key, uint32_t& ke return {PacketAuthenticator::KeyImportStatus::Success, PSA_SUCCESS}; } -PacketAuthenticator::AuthenticationResult authenticatePacket(const uint8_t* dataBuffer, - size_t dataSize, - const Mac& hmac, - uint32_t& keyId) { +PacketAuthenticator::AuthenticationResult authenticateFrame(uint16_t saIndex, + const uint8_t* dataBuffer, + size_t dataSize, + const Mac& hmac, + uint32_t& keyId) { // Basic input validation: buffer present and at least trailer-sized if (!dataBuffer || dataSize < Ccsds355_0_B_2::kTCSecurityTrailer) { return {PacketAuthenticator::AuthenticationStatus::VerifyError, PSA_ERROR_INVALID_ARGUMENT}; } - // Verify the HMAC on the packet data + // The MAC covers the SA index (stripped from the buffer by the upstream deframer) followed by + // the frame data, so it must be verified via the PSA multipart API rather than a single call. const size_t authenticatedDataSize = dataSize - Ccsds355_0_B_2::kTCSecurityTrailer; - const psa_status_t status = - psa_mac_verify(keyId, PSA_ALG_TRUNCATED_MAC(PSA_ALG_HMAC(PSA_ALG_SHA_256), Ccsds355_0_B_2::kTCSecurityTrailer), - dataBuffer, authenticatedDataSize, hmac.data(), hmac.size()); + const uint8_t saIndexBytes[2] = {static_cast(saIndex >> 8), static_cast(saIndex & 0xFF)}; + + psa_mac_operation_t operation = PSA_MAC_OPERATION_INIT; + psa_status_t status = psa_mac_verify_setup( + &operation, keyId, PSA_ALG_TRUNCATED_MAC(PSA_ALG_HMAC(PSA_ALG_SHA_256), Ccsds355_0_B_2::kTCSecurityTrailer)); + if (status != PSA_SUCCESS) { + return {PacketAuthenticator::AuthenticationStatus::VerifyError, status}; + } + + status = psa_mac_update(&operation, saIndexBytes, sizeof(saIndexBytes)); + if (status != PSA_SUCCESS) { + psa_mac_abort(&operation); + return {PacketAuthenticator::AuthenticationStatus::VerifyError, status}; + } + + status = psa_mac_update(&operation, dataBuffer, authenticatedDataSize); + if (status != PSA_SUCCESS) { + psa_mac_abort(&operation); + return {PacketAuthenticator::AuthenticationStatus::VerifyError, status}; + } + + // psa_mac_verify_finish aborts the operation internally on both success and failure + status = psa_mac_verify_finish(&operation, hmac.data(), hmac.size()); if (status != PSA_SUCCESS) { - return PacketAuthenticator::AuthenticationResult{PacketAuthenticator::AuthenticationStatus::VerifyError, - status}; + return {PacketAuthenticator::AuthenticationStatus::VerifyError, status}; } return {PacketAuthenticator::AuthenticationStatus::Authenticated, PSA_SUCCESS}; diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.hpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.hpp similarity index 76% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.hpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.hpp index 296fd038..ebe7a717 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.hpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.hpp @@ -46,11 +46,14 @@ PacketAuthenticator::KeyImportResult importHmacKey(const char* key, //!< The he uint32_t& keyId //!< The key ID to use for the imported key ); -//! Check the validity of the packet HMAC -PacketAuthenticator::AuthenticationResult authenticatePacket( - const uint8_t* buffer, //!< The packet data buffer +//! Check the validity of the frame HMAC. The MAC covers the security association index (2 bytes, +//! big-endian) followed by the frame data; the SA index is supplied separately because the upstream +//! Svc.Ccsds.CcsdsSdlsDeframer has already stripped it out of the buffer. +PacketAuthenticator::AuthenticationResult authenticateFrame( + uint16_t saIndex, //!< The security association index (participates in the MAC but not in the buffer) + const uint8_t* buffer, //!< The frame data buffer size_t size, //!< The size of the data buffer - const Mac& hmac, //!< The HMAC extracted from the packet to validate against + const Mac& hmac, //!< The HMAC extracted from the frame to validate against uint32_t& keyId //!< The hex-encoded authentication key to use for validation ); diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/CMakeLists.txt b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/CMakeLists.txt similarity index 82% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/CMakeLists.txt rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/CMakeLists.txt index 13f05dc8..74feb302 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/CMakeLists.txt +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/CMakeLists.txt @@ -16,10 +16,10 @@ register_fprime_library( AUTOCODER_INPUTS - "${CMAKE_CURRENT_LIST_DIR}/TcSecurityDeframer.fpp" + "${CMAKE_CURRENT_LIST_DIR}/TcSecurityDecryptor.fpp" SOURCES "${CMAKE_CURRENT_LIST_DIR}/Authenticator.cpp" - "${CMAKE_CURRENT_LIST_DIR}/TcSecurityDeframer.cpp" + "${CMAKE_CURRENT_LIST_DIR}/TcSecurityDecryptor.cpp" "${CMAKE_CURRENT_LIST_DIR}/Parser.cpp" "${CMAKE_CURRENT_LIST_DIR}/Validator.cpp" DEPENDS @@ -28,8 +28,8 @@ register_fprime_library( ) # Add mbedTLS include directories for PSA crypto headers -# F Prime creates targets based on path: PROVESFlightControllerReference_Components_TcSecurityDeframer -set(PACKET_PARSER_TARGET "PROVESFlightControllerReference_Components_TcSecurityDeframer") +# F Prime creates targets based on path: PROVESFlightControllerReference_Components_TcSecurityDecryptor +set(PACKET_PARSER_TARGET "PROVESFlightControllerReference_Components_TcSecurityDecryptor") # Add mbedTLS include path - check multiple possible locations if(DEFINED ZEPHYR_MBEDTLS_MODULE_DIR) @@ -51,10 +51,10 @@ endif() ### Unit Tests ### # register_fprime_ut( # AUTOCODER_INPUTS -# "${CMAKE_CURRENT_LIST_DIR}/TcSecurityDeframer.fpp" +# "${CMAKE_CURRENT_LIST_DIR}/TcSecurityDecryptor.fpp" # SOURCES -# "${CMAKE_CURRENT_LIST_DIR}/test/ut/TcSecurityDeframerTestMain.cpp" -# "${CMAKE_CURRENT_LIST_DIR}/test/ut/TcSecurityDeframerTester.cpp" +# "${CMAKE_CURRENT_LIST_DIR}/test/ut/TcSecurityDecryptorTestMain.cpp" +# "${CMAKE_CURRENT_LIST_DIR}/test/ut/TcSecurityDecryptorTester.cpp" # DEPENDS # STest # For rules-based testing # UT_AUTO_HELPERS diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Const.hpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Const.hpp similarity index 68% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/Const.hpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/Const.hpp index 69f14ecc..38d7620e 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Const.hpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Const.hpp @@ -1,6 +1,6 @@ // ====================================================================== // \title Const.cpp -// \brief hpp file for to define constants used by TcSecurityDeframer component +// \brief hpp file for to define constants used by TcSecurityDecryptor component // ====================================================================== #pragma once @@ -16,18 +16,16 @@ namespace Ccsds355_0_B_2 { //! Telecommand packet structure //! CCSDS 355.0-B-2 Section E2.2 +//! The Security Parameter Index is stripped by the upstream Svc.Ccsds.CcsdsSdlsDeframer before +//! this component sees the frame; kSpiSize is kept only for the MAC-prefix fallback in Authenticator. constexpr const size_t kSpiSize = 2; //!< The size of the Security Parameter Index field in bytes constexpr const size_t kSequenceNumberSize = 4; //!< The size of the sequence number field in bytes -constexpr const size_t kTCSecurityHeaderSize = - kSpiSize + kSequenceNumberSize; //!< The telecommand security header size in bytes -constexpr const size_t kTCPrimaryHeaderSize = 5; //!< TC Primary Header is always 5 octets (CCSDS 232.0-B-4) //! CCSDS 355.0-B-2 Section E2.3 constexpr const size_t kTCSecurityTrailer = 16; //!< The telecommand security trailer size in bytes //! Helpers -constexpr const size_t kMinAuthenticatedPacketSize = - kTCSecurityHeaderSize + kTCSecurityTrailer; //!< Minimum packet size +constexpr const size_t kMinAuthenticatedPacketSize = kSequenceNumberSize + kTCSecurityTrailer; //!< Minimum packet size } // namespace Ccsds355_0_B_2 diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.cpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.cpp similarity index 70% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.cpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.cpp index e5d1d08e..50eb1df8 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.cpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.cpp @@ -19,30 +19,17 @@ struct FieldParseResult { T value; //!< The parsed value }; -//! Parse the SPI field from the packet buffer -FieldParseResult parseSpi(const uint8_t* buffer, const size_t size) { - // Validate buffer size - if (!buffer || size < Ccsds355_0_B_2::kSpiSize) { - return {false, 0}; - } - - // Extract SPI - const uint32_t spi = (static_cast(buffer[0]) << 8) | static_cast(buffer[1]); - - return {true, spi}; -} - //! Parse the sequence number field from the packet buffer FieldParseResult parseSequenceNumber(const uint8_t* buffer, const size_t size) { // Validate buffer size - if (!buffer || size < Ccsds355_0_B_2::kTCSecurityHeaderSize) { + if (!buffer || size < Ccsds355_0_B_2::kSequenceNumberSize) { return {false, 0}; } // Extract sequence number - const uint32_t sequenceNumber = (static_cast(buffer[2]) << 24) | - (static_cast(buffer[3]) << 16) | (static_cast(buffer[4]) << 8) | - static_cast(buffer[5]); + const uint32_t sequenceNumber = (static_cast(buffer[0]) << 24) | + (static_cast(buffer[1]) << 16) | (static_cast(buffer[2]) << 8) | + static_cast(buffer[3]); return {true, sequenceNumber}; } @@ -66,12 +53,6 @@ FieldParseResult parseMac(const uint8_t* buffer, const size_t size) { namespace Ccsds355_0_B_2 { TcTransferFrame::Parser::Result parse(const uint8_t* buffer, const size_t size) { - // Parse SPI - const FieldParseResult spiResult = parseSpi(buffer, size); - if (!spiResult.success) { - return {Ccsds355_0_B_2::TcTransferFrame::Parser::Status::SpiParseError, {}}; - } - // Parse sequence number const FieldParseResult sequenceNumberResult = parseSequenceNumber(buffer, size); if (!sequenceNumberResult.success) { @@ -84,9 +65,9 @@ TcTransferFrame::Parser::Result parse(const uint8_t* buffer, const size_t size) return {Ccsds355_0_B_2::TcTransferFrame::Parser::Status::MacParseError, {}}; } - return {TcTransferFrame::Parser::Status::Ok, TCSecurityHeader{spiResult.value, sequenceNumberResult.value}, + return {TcTransferFrame::Parser::Status::Ok, TCSecurityHeader{sequenceNumberResult.value}, TCSecurityTrailer{macResult.value}, - TCFrameData{buffer + kTCSecurityHeaderSize, size - kTCSecurityHeaderSize - kTCSecurityTrailer}}; + TCFrameData{buffer + kSequenceNumberSize, size - kSequenceNumberSize - kTCSecurityTrailer}}; } } // namespace Ccsds355_0_B_2 diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.hpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.hpp similarity index 88% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.hpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.hpp index 58cca1b5..d8fa7b5b 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.hpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.hpp @@ -18,7 +18,6 @@ namespace Parser { //! Must match the ParserStatus enum in the .fpp file enum class Status { Ok, //!< Transfer frame was successfully parsed - SpiParseError, //!< SPI could not be parsed from transfer frame SequenceNumberParseError, //!< Sequence number could not be parsed from transfer frame MacParseError, //!< MAC could not be parsed from transfer frame }; @@ -35,7 +34,9 @@ struct Result { } // namespace Parser } // namespace TcTransferFrame -//! Parse the TC Transfer Frame from a buffer and extract relevant information for validation and authentication +//! Parse the TC Transfer Frame from a buffer and extract relevant information for validation and authentication. +//! The buffer is [SeqNum(4) | Data | MAC(16)]: the Security Parameter Index has already been stripped +//! by the upstream Svc.Ccsds.CcsdsSdlsDeframer. TcTransferFrame::Parser::Result parse(const uint8_t* buffer, //!< The transfer frame buffer const size_t size //!< The transfer frame size ); diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.cpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.cpp similarity index 73% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.cpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.cpp index 3c94f9fb..76c87c68 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.cpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.cpp @@ -1,16 +1,16 @@ // ====================================================================== -// \title TcSecurityDeframer.cpp -// \brief cpp file for TcSecurityDeframer component implementation class +// \title TcSecurityDecryptor.cpp +// \brief cpp file for TcSecurityDecryptor component implementation class // ====================================================================== -#include "PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.hpp" +#include "PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.hpp" #include #include #include #include "Authenticator.hpp" -#include "TcSecurityDeframer.hpp" +#include "TcSecurityDecryptor.hpp" #include "Types.hpp" // Include generated header with default key (generated at build time) @@ -22,33 +22,37 @@ namespace Components { // Component construction and destruction // ---------------------------------------------------------------------- -TcSecurityDeframer ::TcSecurityDeframer(const char* const compName) - : TcSecurityDeframerComponentBase(compName), +TcSecurityDecryptor ::TcSecurityDecryptor(const char* const compName) + : TcSecurityDecryptorComponentBase(compName), m_sequenceNumberFilePath(), m_sequenceNumber(0), m_sequenceNumberWindow(0) {} -TcSecurityDeframer ::~TcSecurityDeframer() {} +TcSecurityDecryptor ::~TcSecurityDecryptor() {} // ---------------------------------------------------------------------- // Handler implementations for typed input ports // ---------------------------------------------------------------------- -void TcSecurityDeframer ::dataIn_handler(FwIndexType portNum, Fw::Buffer& data, const ComCfg::FrameContext& context) { +void TcSecurityDecryptor ::decryptIn_handler(FwIndexType portNum, + U16 saIndex, + Fw::Buffer& data, + const ComCfg::FrameContext& context) { ComCfg::FrameContext contextOut = context; contextOut.set_authenticated(false); - // TcDeframer has already stripped the TC Primary Header and FECF, so the buffer is: - // [Security Header: SPI(2)+SeqNum(4)] [Data Field] [Security Trailer: MAC(16)] + // The upstream Svc.Ccsds.CcsdsSdlsDeframer has already stripped the security association + // index, so the buffer is: [SeqNum(4)] [Data Field] [Security Trailer: MAC(16)] - // --- Parse Security Header and Trailer --- + // --- Parse Security Trailer --- const Ccsds355_0_B_2::TcTransferFrame::Parser::Result parseResult = Ccsds355_0_B_2::parse(data.getData(), data.getSize()); if (parseResult.status != Ccsds355_0_B_2::TcTransferFrame::Parser::Status::Ok) { // The frame is too short to contain the security fields, so it cannot be stripped - // for downstream deframing. Return buffer ownership upstream and drop the frame. + // for downstream deframing. Report a decryption failure so the upstream deframer + // drops the frame and returns the buffer for deallocation. this->log_WARNING_HI_ParsingFailed(static_cast(parseResult.status)); - this->dataReturnOut_out(0, data, contextOut); + this->decryptOut_out(0, Svc::Ccsds::SdlsStatus::DECRYPTION_FAILURE, data, contextOut); return; } this->log_WARNING_HI_ParsingFailed_ThrottleClear(); @@ -56,12 +60,12 @@ void TcSecurityDeframer ::dataIn_handler(FwIndexType portNum, Fw::Buffer& data, { Os::ScopeLock lock(this->m_sequenceNumberLock); - // --- Validate SPI and anti-replay sequence number --- - const PacketValidator::Status validationStatus = - validatePacket(parseResult.securityHeader, this->m_sequenceNumber, this->m_sequenceNumberWindow); + // --- Validate SA index and anti-replay sequence number --- + const PacketValidator::Status validationStatus = validateFrame( + saIndex, parseResult.securityHeader.sequenceNumber, this->m_sequenceNumber, this->m_sequenceNumberWindow); if (validationStatus == PacketValidator::Status::SpiInvalid) { - this->log_WARNING_HI_SpiInvalid(parseResult.securityHeader.spi); + this->log_WARNING_HI_SpiInvalid(saIndex); } else if (validationStatus == PacketValidator::Status::SequenceNumberInvalid) { this->log_WARNING_HI_SequenceNumberInvalid(parseResult.securityHeader.sequenceNumber, this->m_sequenceNumber, this->m_sequenceNumberWindow); @@ -69,9 +73,9 @@ void TcSecurityDeframer ::dataIn_handler(FwIndexType portNum, Fw::Buffer& data, this->log_WARNING_HI_SpiInvalid_ThrottleClear(); this->log_WARNING_HI_SequenceNumberInvalid_ThrottleClear(); - // --- Authenticate: HMAC over Security Header + Data Field --- - const PacketAuthenticator::AuthenticationResult authResult = - authenticatePacket(data.getData(), data.getSize(), parseResult.securityTrailer.mac, this->m_hmacKeyId); + // --- Authenticate: HMAC over SA index + Data Field --- + const PacketAuthenticator::AuthenticationResult authResult = authenticateFrame( + saIndex, data.getData(), data.getSize(), parseResult.securityTrailer.mac, this->m_hmacKeyId); if (authResult.status != PacketAuthenticator::AuthenticationStatus::Authenticated) { this->log_WARNING_HI_AuthenticationFailed(static_cast(authResult.status), @@ -91,32 +95,29 @@ void TcSecurityDeframer ::dataIn_handler(FwIndexType portNum, Fw::Buffer& data, } // Forward only the Data Field per CCSDS 355.0-B-2 ยง3.3.3.3: - // start = first octet after Security Header + // start = first octet after the sequence number // end = last octet of the Transfer Frame Data Field (excluding Security Trailer) - // Unverified frames are forwarded with authenticated=false; the router enforces - // the reject-or-bypass policy. - data.advance(Ccsds355_0_B_2::kTCSecurityHeaderSize); + // Unverified frames are forwarded with authenticated=false; the router enforces the + // reject-or-bypass policy. Every structurally parseable frame reports SUCCESS: an + // authentication-only SA has no notion of a decrypt failure short of a parse error, and + // upstream drops any non-SUCCESS frame, which would defeat the #426 bypass path. + data.advance(Ccsds355_0_B_2::kSequenceNumberSize); data.setSize(data.getSize() - Ccsds355_0_B_2::kTCSecurityTrailer); - this->dataOut_out(0, data, contextOut); + this->decryptOut_out(0, Svc::Ccsds::SdlsStatus::SUCCESS, data, contextOut); } -void TcSecurityDeframer ::dataReturnIn_handler(FwIndexType portNum, - Fw::Buffer& data, - const ComCfg::FrameContext& context) { - // Restore the original buffer pointer and size stripped in dataIn_handler so the - // upstream BufferManager deallocates the exact allocation it originally handed out. - data.advance(-static_cast(Ccsds355_0_B_2::kTCSecurityHeaderSize)); - data.setSize(data.getSize() + Ccsds355_0_B_2::kTCSecurityTrailer); - - this->dataReturnOut_out(0, data, context); +void TcSecurityDecryptor ::decryptReturnIn_handler(FwIndexType portNum, + Fw::Buffer& data, + const ComCfg::FrameContext& context) { + this->bufferReturnOut_out(0, data, context); } // ---------------------------------------------------------------------- // Handler implementations for commands // ---------------------------------------------------------------------- -void TcSecurityDeframer ::GET_SEQ_NUM_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { +void TcSecurityDecryptor ::GET_SEQ_NUM_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { Os::ScopeLock lock(this->m_sequenceNumberLock); // Log the successful sequence number get @@ -126,7 +127,7 @@ void TcSecurityDeframer ::GET_SEQ_NUM_cmdHandler(FwOpcodeType opCode, U32 cmdSeq this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } -void TcSecurityDeframer ::SET_SEQ_NUM_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U32 seq_num) { +void TcSecurityDecryptor ::SET_SEQ_NUM_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U32 seq_num) { Os::ScopeLock lock(this->m_sequenceNumberLock); // Write the sequence number to the file system @@ -154,7 +155,7 @@ void TcSecurityDeframer ::SET_SEQ_NUM_cmdHandler(FwOpcodeType opCode, U32 cmdSeq // Public helper methods // ---------------------------------------------------------------------- -void TcSecurityDeframer ::configure() { +void TcSecurityDecryptor ::configure() { Os::ScopeLock lock(this->m_sequenceNumberLock); Fw::ParamValid is_valid; @@ -185,7 +186,7 @@ void TcSecurityDeframer ::configure() { // Private helper methods // ---------------------------------------------------------------------- -Os::File::Status TcSecurityDeframer ::readSequenceNumber(U32& value) { +Os::File::Status TcSecurityDecryptor ::readSequenceNumber(U32& value) { // Read the sequence number from the file system Os::File::Status status = Utilities::FileHelper::readFromFile(this->m_sequenceNumberFilePath.toChar(), value); if (status != Os::File::OP_OK) { @@ -204,7 +205,7 @@ Os::File::Status TcSecurityDeframer ::readSequenceNumber(U32& value) { return status; } -Os::File::Status TcSecurityDeframer ::writeSequenceNumber(const U32 value) { +Os::File::Status TcSecurityDecryptor ::writeSequenceNumber(const U32 value) { Os::File::Status status = Utilities::FileHelper::writeToFile(this->m_sequenceNumberFilePath.toChar(), value); if (status != Os::File::OP_OK) { // Log the failure to write the default sequence number diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.fpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.fpp similarity index 77% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.fpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.fpp index f4b0d7c7..af632304 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.fpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.fpp @@ -8,18 +8,20 @@ module Components { @ FPP shadow-enum representing Components::PacketParser::Status enum PacketParserStatus { Ok, @< Packet was successfully parsed - SpiParseError, @< SPI could not be parsed from packet SequenceNumberParseError, @< Sequence number could not be parsed from packet MacParseError, @< MAC could not be parsed from packet } - @ Component placed between the TcDeframer and SpacePacketDeframer components. It - @ implements the TC ProcessSecurity flow of CCSDS 355.0-B-2: parse the Security - @ Header and Trailer, validate the SPI and anti-replay sequence number, and verify - @ the MAC. The verification result is recorded in the frame context - @ (authenticated flag); policy enforcement (reject or bypass) is owned downstream - @ by the router. - passive component TcSecurityDeframer { + @ Decryptor behind the upstream Svc.Ccsds.CcsdsSdlsDeframer, implementing + @ Svc.Ccsds.CcsdsSdlsDecrypt. Implements the TC ProcessSecurity flow of CCSDS 355.0-B-2: + @ parse the Security Trailer, validate the security association index and anti-replay + @ sequence number, and verify the MAC (which covers the security association index, + @ supplied by the deframer, and the frame data). The verification result is recorded in + @ the frame context (authenticated flag); policy enforcement (reject or bypass) is owned + @ downstream by the router. + passive component TcSecurityDecryptor { + + import Svc.Ccsds.CcsdsSdlsDecrypt ### Commands ### @@ -57,8 +59,8 @@ module Components { @ ParsingFailed indicates that there was an error parsing a received packet event ParsingFailed(parse_status: PacketParserStatus) severity warning high id 3 format "Parsing failed: {}" throttle 2 - @ SpiInvalid indicates that a received packet had an invalid SPI value - event SpiInvalid(packet_spi: U32) severity warning high id 4 format "SPI invalid: Received={}" throttle 2 + @ SpiInvalid indicates that a received packet had an invalid security association index + event SpiInvalid(sa_index: U32) severity warning high id 4 format "Security association index invalid: Received={}" throttle 2 ### Parameters ### @@ -68,20 +70,6 @@ module Components { @ Parameter for the file path where the current sequence number is stored param SEQ_NUM_FILE_PATH : string default "//sequence_number.txt" - ### Ports ### - - @ Port receiving frames from TcDeframer: [Security Header | Data Field | Security Trailer] - guarded input port dataIn: Svc.ComDataWithContext - - @ Port forwarding the Data Field to SpacePacketDeframer with the authenticated flag set in the context - output port dataOut: Svc.ComDataWithContext - - @ Port returning ownership of structurally invalid frames back to TcDeframer - output port dataReturnOut: Svc.ComDataWithContext - - @ Port receiving back ownership of buffers sent on dataOut - sync input port dataReturnIn: Svc.ComDataWithContext - ############################################################################### # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # ############################################################################### diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.hpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.hpp similarity index 65% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.hpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.hpp index 08040a52..138b96c5 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.hpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptor.hpp @@ -1,10 +1,10 @@ // ====================================================================== -// \title TcSecurityDeframer.hpp -// \brief hpp file for TcSecurityDeframer component implementation class +// \title TcSecurityDecryptor.hpp +// \brief hpp file for TcSecurityDecryptor component implementation class // ====================================================================== -#ifndef Components_TcSecurityDeframer -#define Components_TcSecurityDeframer +#ifndef Components_TcSecurityDecryptor +#define Components_TcSecurityDecryptor #include #include @@ -13,50 +13,50 @@ #include #include -#include "PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.hpp" -#include "PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.hpp" -#include "PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframerComponentAc.hpp" -#include "PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.hpp" +#include "PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.hpp" +#include "PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.hpp" +#include "PROVESFlightControllerReference/Components/TcSecurityDecryptor/TcSecurityDecryptorComponentAc.hpp" +#include "PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.hpp" namespace Components { -class TcSecurityDeframer final : public TcSecurityDeframerComponentBase { +class TcSecurityDecryptor final : public TcSecurityDecryptorComponentBase { public: // ---------------------------------------------------------------------- // Component construction and destruction // ---------------------------------------------------------------------- - //! Construct TcSecurityDeframer object - TcSecurityDeframer(const char* const compName //!< The component name + //! Construct TcSecurityDecryptor object + TcSecurityDecryptor(const char* const compName //!< The component name ); - //! Destroy TcSecurityDeframer object - ~TcSecurityDeframer(); + //! Destroy TcSecurityDecryptor object + ~TcSecurityDecryptor(); private: // ---------------------------------------------------------------------- // Handler implementations for typed input ports // ---------------------------------------------------------------------- - //! Handler implementation for dataIn + //! Handler implementation for decryptIn //! - //! Receives [Security Header | Data Field | Security Trailer] from TcDeframer (which - //! has already stripped the TC Primary Header and FECF), verifies the Security Header - //! and MAC, records the result in the frame context authenticated flag, and forwards - //! the Data Field per CCSDS 355.0-B-2 ยง3.3.3.3. Structurally invalid frames are - //! returned upstream on dataReturnOut. - void dataIn_handler(FwIndexType portNum, //!< The port number - Fw::Buffer& data, //!< The frame buffer - const ComCfg::FrameContext& context //!< The frame context - ) override; - - //! Handler implementation for dataReturnIn + //! Receives the security association index and [SeqNum | Data | Security Trailer] from the + //! upstream Svc.Ccsds.CcsdsSdlsDeframer (which has already stripped the SA index out of the + //! buffer), verifies the anti-replay sequence number and MAC, records the result in the frame + //! context authenticated flag, and forwards the Data Field per CCSDS 355.0-B-2 ยง3.3.3.3. + void decryptIn_handler(FwIndexType portNum, //!< The port number + U16 saIndex, //!< The security association index + Fw::Buffer& data, //!< The frame buffer + const ComCfg::FrameContext& context //!< The frame context + ) override; + + //! Handler implementation for decryptReturnIn //! - //! Returns ownership of buffers sent on dataOut back to the upstream component - void dataReturnIn_handler(FwIndexType portNum, //!< The port number - Fw::Buffer& data, //!< The frame buffer - const ComCfg::FrameContext& context //!< The frame context - ) override; + //! Returns ownership of buffers sent on decryptOut back to the upstream deframer + void decryptReturnIn_handler(FwIndexType portNum, //!< The port number + Fw::Buffer& data, //!< The frame buffer + const ComCfg::FrameContext& context //!< The frame context + ) override; private: // ---------------------------------------------------------------------- @@ -114,4 +114,4 @@ class TcSecurityDeframer final : public TcSecurityDeframerComponentBase { } // namespace Components -#endif // Components_TcSecurityDeframer +#endif // Components_TcSecurityDecryptor diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Types.hpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Types.hpp similarity index 88% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/Types.hpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/Types.hpp index 3d4b5937..ab68b960 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Types.hpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Types.hpp @@ -1,6 +1,6 @@ // ====================================================================== // \title Types.cpp -// \brief hpp file for to define types used by TcSecurityDeframer component +// \brief hpp file for to define types used by TcSecurityDecryptor component // ====================================================================== #pragma once @@ -18,9 +18,10 @@ using Mac = std::array; //!< The M //! https://ccsds.org/Pubs/355x0b2.pdf namespace Ccsds355_0_B_2 { -//! Describes the frame security header format for a Telecommand (TC) Transfer Frame +//! Describes the frame security header format for a Telecommand (TC) Transfer Frame. The Security +//! Parameter Index is not part of this header: it is stripped and delivered separately (as saIndex) +//! by the upstream Svc.Ccsds.CcsdsSdlsDeframer. struct TCSecurityHeader { - uint32_t spi; //!< The Security Parameter Index of the packet uint32_t sequenceNumber; //!< The sequence number of the packet }; diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.cpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.cpp similarity index 71% rename from PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.cpp rename to PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.cpp index 0e494f34..336d6341 100644 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.cpp +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.cpp @@ -8,10 +8,10 @@ namespace Components { namespace { -//! Validate the SPI field of the packet -bool spiValid(uint32_t spi) { - // For now we only support SPI 0, which indicates no additional security processing beyond HMAC - return spi == 0; +//! Validate the security association index +bool spiValid(uint32_t saIndex) { + // For now we only support SA index 0, which indicates no additional security processing beyond HMAC + return saIndex == 0; } //! Validate packet sequence number must be greater than the last accepted sequence number and within the window @@ -33,14 +33,15 @@ bool sequenceNumberValid(uint32_t packetSequenceNumber, uint32_t sequenceNumber, } // namespace -PacketValidator::Status validatePacket(const Ccsds355_0_B_2::TCSecurityHeader& secHeader, - uint32_t sequenceNumber, - uint32_t sequenceNumberWindow) { - if (!spiValid(secHeader.spi)) { +PacketValidator::Status validateFrame(uint32_t saIndex, + uint32_t packetSequenceNumber, + uint32_t sequenceNumber, + uint32_t sequenceNumberWindow) { + if (!spiValid(saIndex)) { return PacketValidator::Status::SpiInvalid; } - if (!sequenceNumberValid(secHeader.sequenceNumber, sequenceNumber, sequenceNumberWindow)) { + if (!sequenceNumberValid(packetSequenceNumber, sequenceNumber, sequenceNumberWindow)) { return PacketValidator::Status::SequenceNumberInvalid; } diff --git a/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.hpp b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.hpp new file mode 100644 index 00000000..e3494ab6 --- /dev/null +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.hpp @@ -0,0 +1,33 @@ +// ====================================================================== +// \title Validator.hpp +// \brief hpp file for packet policy validation helper functions +// ====================================================================== + +#pragma once + +#include +#include + +#include "Types.hpp" + +namespace Components { +namespace PacketValidator { + +//! Status of validation attempt +enum class Status { + Valid, //!< The packet is valid and passes all checks + SpiInvalid, //!< The security association index is invalid + SequenceNumberInvalid, //!< The packet sequence number is outside the acceptable window +}; + +} // namespace PacketValidator + +//! Validate the frame against ruleset. The security association index is supplied by the caller +//! (sourced from the upstream Svc.Ccsds.CcsdsSdlsDeframer) rather than parsed from the frame. +PacketValidator::Status validateFrame(uint32_t saIndex, //!< The security association index + uint32_t packetSequenceNumber, //!< The sequence number parsed from the frame + uint32_t sequenceNumber, //!< The current sequence number + uint32_t sequenceNumberWindow //!< The acceptable sequence number window +); + +} // namespace Components diff --git a/PROVESFlightControllerReference/Components/TcSecurityDecryptor/docs/sdd.md b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/docs/sdd.md new file mode 100644 index 00000000..e3946f82 --- /dev/null +++ b/PROVESFlightControllerReference/Components/TcSecurityDecryptor/docs/sdd.md @@ -0,0 +1,280 @@ +# Components::TcSecurityDecryptor + +The TcSecurityDecryptor component implements the TC ProcessSecurity flow of CCSDS 355.0-B-2 in the +uplink path, acting as the decryptor behind the upstream F Prime `Svc::Ccsds::CcsdsSdlsDeframer` +(introduced in F Prime 4.3.0). The SDLS deframer extracts the 2-byte security association (SA) +index from the frame and calls this component via the `Svc.Ccsds.CcsdsSdlsDecrypt` interface; this +component validates the anti-replay sequence number, verifies the HMAC (which covers the SA index +and the frame data), then strips the remaining security envelope and forwards the frame with the +verification result recorded in the frame context (`authenticated` flag). + +The component does not enforce policy. Frames that fail verification are still forwarded +(unauthenticated) so that downstream policy โ€” owned by ProvesRouter and its opcode bypass +allowlist โ€” can decide whether to route or reject them. This keeps knowledge of packet structure +here and knowledge of policy at the edge. + +## Relationship to CcsdsSdlsDeframer + +This project previously owned the full deframing/SA-extraction step in a single component +(`TcSecurityDeframer`, a pass-through between `TcDeframer` and `SpacePacketDeframer`) because stock +F Prime had no security hook. F Prime 4.3.0 added that hook, so responsibility is now split: + +- `Svc::Ccsds::CcsdsSdlsDeframer` (upstream, unmodified): strips the 2-byte SA index from the frame + and calls the decryptor with the SA index and the remaining `[SeqNum | Data | MAC]` buffer. +- `Components::TcSecurityDecryptor` (this component): validates and authenticates using the SA + index supplied by the deframer, strips the sequence number and MAC, and reports the result. + +Two deliberate deviations from a "default" upstream integration: + +1. **No `Svc::Ccsds::SdlsSaRouter`.** The router selects a *component* by SA index; issue #472's + design selects a *key* by SA index inside this component's 2-slot key store. The router would + add a passive component and a tracking table per subtopology (three on the rp2350) for no + benefit today. `sdlsDeframer.decryptOut` connects directly to this component; inserting a router + later is a topology-only change if a project ever needs to dispatch to multiple decryptor + implementations by SA index. +2. **`SdlsStatus::SUCCESS` returned for every structurally parseable frame.** Upstream's + `CcsdsSdlsDeframer` drops any frame whose decrypt status is not `SUCCESS`, which would defeat the + #426 bypass path (unauthenticated `CMD_NO_OP` must still reach `ProvesRouter` so its allowlist can + accept it). This component therefore expresses authenticity solely via + `context.authenticated` and returns `DECRYPTION_FAILURE` only when the frame is too short to + parse, where dropping is correct regardless of authentication policy. This is a deliberate + reading of an authentication-only security association, not a bug: there is no separate notion + of "decryption failure" when the SA performs no decryption. + +### Why not `Svc::Ccsds::SdlsFileKeyManager` + +Upstream's `SdlsFileKeyManager` and the `Svc.Ccsds.SdlsKeyInterface` port were not adopted. The +upstream `SdlsKey` port carries no SA index, so it cannot express the 2-slot, SPI-keyed key store +that issue #472 needs, and #472/#490 put `PROVISION_KEY`/`ADD_KEY`/`REMOVE_KEY`/`GET_ACTIVE_KEYS` +commands directly on this component. Key material therefore stays inside this component's shell; +`SdlsFileKeyManager` would have to grow an SA-index argument on every port and duplicate the command +surface #472 already defines here, which is strictly more code for no capability gain. Adopting it +would become worth revisiting only if a future SA needed to share key material across multiple +security components. + +## Overview + +The component is a thin stateful shell over pure-function namespaces: + +- `Ccsds355_0_B_2::parse` (Parser) โ€” sequence number and Security Trailer (MAC) extraction from + `[SeqNum | Data | MAC]` (the SA index is not part of this buffer; it arrives as a port argument) +- `Components::validateFrame` (Validator) โ€” SA index validation and anti-replay sequence-number + window validation +- `Components::authenticateFrame` / `importHmacKey` (Authenticator) โ€” HMAC-SHA-256 (truncated to 16 + bytes) verification via the PSA multipart MAC API, covering the SA index followed by the frame + data + +The only component state is the last accepted sequence number (mutex-guarded, persisted to file) +and the imported HMAC key id. + +Primary data path connections (per uplink subtopology): + +- `sdlsDeframer.decryptOut -> tcSecurityDecryptor.decryptIn` +- `tcSecurityDecryptor.decryptOut -> sdlsDeframer.decryptIn` +- `sdlsDeframer.decryptReturnOut -> tcSecurityDecryptor.decryptReturnIn` +- `tcSecurityDecryptor.bufferReturnOut -> sdlsDeframer.bufferReturnIn` + +## Class Diagram + +```mermaid +classDiagram +direction LR + +class TcSecurityDecryptor { + +configure() + -decryptIn_handler(portNum, saIndex, data, context) + -decryptReturnIn_handler(portNum, data, context) + -GET_SEQ_NUM_cmdHandler(opCode, cmdSeq) + -SET_SEQ_NUM_cmdHandler(opCode, cmdSeq, seqNum) + -readSequenceNumber(value) + -writeSequenceNumber(value) + -m_sequenceNumber : U32 + -m_sequenceNumberWindow : U32 + -m_hmacKeyId : uint32_t +} + +class Ccsds355_0_B_2 { + <> + +parse(buffer, size) Parser_Result +} + +class PacketValidator { + <> + +validateFrame(saIndex, packetSeqNum, sequenceNumber, window) Status +} + +class PacketAuthenticator { + <> + +importHmacKey(key, keyId) KeyImportResult + +authenticateFrame(saIndex, buffer, size, mac, keyId) AuthenticationResult +} + +class TCSecurityHeader { + +sequenceNumber : uint32_t +} + +class TCSecurityTrailer { + +mac : Mac +} + +class Mac { + <> + +std::array~uint8_t,16~ +} + +TcSecurityDecryptor ..> Ccsds355_0_B_2 : parses +TcSecurityDecryptor ..> PacketValidator : validates +TcSecurityDecryptor ..> PacketAuthenticator : authenticates +Ccsds355_0_B_2 --> TCSecurityHeader : returns +Ccsds355_0_B_2 --> TCSecurityTrailer : returns +TCSecurityTrailer --> Mac : contains +``` + +## Packet Format + +The upstream `Svc::Ccsds::CcsdsSdlsDeframer` strips the SA index (2 bytes) before calling this +component, so the buffer received on `decryptIn` is: + +- Sequence Number (4 bytes) +- Space Packet Primary Header (6 bytes) +- Space Packet Data Field (includes F Prime command) +- Security Trailer (16-byte MAC) + +Output packet layout (forwarded per CCSDS 355.0-B-2 ยง3.3.3.3): + +- Space Packet Primary Header (6 bytes) +- Space Packet Data Field + +The MAC is HMAC-SHA-256 truncated to 16 bytes, computed over the SA index (2 bytes, big-endian) and +the Data Field (everything except the Security Trailer). Because the SA index is stripped from the +buffer by the deframer before this component sees it, verification uses the PSA multipart MAC API +(`psa_mac_verify_setup` โ†’ `update(saIndex)` โ†’ `update(data)` โ†’ `psa_mac_verify_finish`) rather than a +single `psa_mac_verify` call. The ground-side `Framing/src/authenticate_plugin.py` is unchanged: it +still emits `SPI(2) | SeqNum(4) | Data | MAC(16)` and MACs `SPI | SeqNum | Data`. + +### Additional resources + +- [CCSDS 355.0-B-2 Space Data Link Security Protocol](https://ccsds.org/Pubs/355x0b2.pdf) + +## Behavior + +1. Parse the sequence number and Security Trailer. If the frame is too short to contain them it + cannot be stripped for downstream deframing: log ParsingFailed and report + `SdlsStatus::DECRYPTION_FAILURE` on `decryptOut` (the upstream deframer drops the frame, notifies + `errorNotify`, and hands the buffer back). +2. Validate the SA index (only SA 0 is currently supported) and the anti-replay sequence number + (must be strictly ahead of the last accepted value, within SEQ_NUM_WINDOW, with U32 wraparound + handled). +3. If validation passes, verify the MAC. +4. Only when all checks pass: store and persist the received sequence number, telemeter it, and set + `authenticated = true` in the frame context. Frames failing any check never advance the sequence + number (issue #426). +5. Strip the sequence number and Security Trailer and forward on `decryptOut` with + `SdlsStatus::SUCCESS` and the resulting `authenticated` flag (see "Relationship to + CcsdsSdlsDeframer" above for why every parseable frame reports `SUCCESS`). ProvesRouter rejects + unauthenticated packets unless their opcode is on the bypass allowlist. + +At startup, `configure()` loads the persisted sequence number and telemeters it so the first +downlinked value is correct before any command is accepted (issue #427). + +## Parameters + +| Name | Type | Default | Description | +|---|---|---|---| +| SEQ_NUM_WINDOW | U32 | 50000 | Maximum allowed forward sequence-number distance before rejecting a packet as out-of-window. | +| SEQ_NUM_FILE_PATH | string | "//sequence_number.txt" | File path used to persist and restore the sequence number across restarts. | + +## Port Descriptions + +| Name | Direction | Type | Description | +|---|---|---|---| +| decryptIn | Input (guarded) | Svc.Ccsds.CcsdsSdlsEncryption | Receives the SA index and `[SeqNum \| Data \| MAC]` buffer from the upstream SDLS deframer. | +| decryptReturnIn | Input (guarded) | Svc.ComDataWithContext | Receives back ownership of buffers previously sent on decryptOut. | +| decryptOut | Output | Svc.Ccsds.CcsdsSdlsData | Forwards the decrypt status and stripped frame downstream with the authenticated flag set in the context. | +| bufferReturnOut | Output | Svc.ComDataWithContext | Returns ownership of the incoming iv/data buffer (relays decryptReturnIn ownership upstream to the deframer). | + +Standard AC ports are also present for command handling, events, telemetry, parameter access, and +time. + +## Telemetry Channels + +| Name | Type | Description | +|---|---|---| +| CurrentSequenceNumber | U32 | Current accepted sequence number tracked by the component. Emitted at startup and on each accepted packet. | + +Routed/bypassed/rejected packet counts are telemetered by ProvesRouter, which owns the accept/reject policy. + +## Events + +| Name | Severity | Parameters | Description | +|---|---|---|---| +| SequenceNumberGet | Activity High | seq_num: U32 | Logged by GET_SEQ_NUM on successful read. Format: "Sequence number is {}" | +| SequenceNumberReadFailed | Warning High (throttle 2) | status: Os.FileStatus | Logged when sequence-number read fails. Format: "Failed to read sequence number, error: {}" | +| SequenceNumberSet | Activity High | seq_num: U32 | Logged by SET_SEQ_NUM on successful write. Format: "Sequence number set to {}" | +| SequenceNumberWriteFailed | Warning High (throttle 2) | status: Os.FileStatus | Logged when sequence-number write fails. Format: "Failed to write sequence number, error: {}" | +| SequenceNumberInvalid | Warning High (throttle 2) | packet_seq_num: U32, seq_num: U32, window: U32 | Logged when anti-replay validation fails. Format: "Sequence number less than last accepted or out of window: Received={}, LastAccepted={}, Window={}" | +| AuthenticationFailed | Warning High (throttle 2) | auth_status: PacketAuthenticatorStatus, rc: I32 | Logged when MAC verification fails. Format: "Authentication failed: Status={}, PSA Return Code={}" | +| ParsingFailed | Warning High (throttle 2) | parse_status: PacketParserStatus | Logged when frame parsing fails. Format: "Parsing failed: {}" | +| SpiInvalid | Warning High (throttle 2) | sa_index: U32 | Logged when SA index validation fails. Format: "Security association index invalid: Received={}" | + +## Commands + +| Name | Type | Parameters | Description | +|---|---|---|---| +| GET_SEQ_NUM | Sync | None | Reads and reports the current sequence number (SequenceNumberGet event). | +| SET_SEQ_NUM | Sync | seq_num: U32 | Sets and persists a new sequence number (SequenceNumberSet event). | + +## Unit Tests + +TcSecurityDecryptor helper functionality is covered by unit tests in PROVESFlightControllerReference/test/unit-tests: + +| Test File | Coverage | +|---|---| +| test_TcSecurityDecryptor_Parser.cpp | Valid parse path plus parse failures for sequence number and MAC size checks. | +| test_TcSecurityDecryptor_Validator.cpp | SA index validation, out-of-window and replayed sequence numbers, window boundary, and wraparound handling. | +| test_TcSecurityDecryptor_Authenticator.cpp | Key import failures, successful MAC verification, failed verification with corrupted MAC or data, and coverage that the SA index participates in the MAC (the same `[data\|MAC]` bytes verify under SA 0 but fail under SA 1). | + +Run unit tests with: + +```bash +make test-unit +``` + +## GDS Plugin + +To send authenticated packets from GDS, build the framing plugin: + +```bash +make framer-plugin +``` + +Then run GDS with the framing plugin enabled as configured by the project tooling. + +## Generating Keys + +The default authentication key header (AuthDefaultKey.h) is generated at build time from project key material via `make generate-auth-key` or `make copy-secrets`. This generated file is machine-local and not committed. + +## Requirements + +| Name | Description | Validation | +|---|---|---| +| AUTH001 | The component shall parse incoming frames to extract the sequence number and MAC fields. | Unit Test | +| AUTH003 | The component shall validate that the security association index supplied by the upstream deframer corresponds to a configured Security Association. | Unit Test | +| AUTH004 | The component shall validate the received sequence number against the stored sequence number. | Unit Test | +| AUTH004-A | The component shall not authenticate packets with sequence numbers that are outside the acceptable window and shall log an event. | Unit Test, Inspection | +| AUTH004-B | The component shall set the stored sequence number to the sequence number transmitted in the packet only when a packet is fully validated and authenticated. | Inspection | +| AUTH004-C | The component shall allow the sequence number window to be configurable via a parameter. | Inspection | +| AUTH005 | The component shall compute the MAC over the security association index and the frame data minus the 16-byte security trailer. | Unit Test | +| AUTH005-A | The component shall not mark packets as authenticated where the computed MAC does not match the security trailer MAC. | Unit Test | +| AUTH006 | For any parseable frame, the component shall remove the sequence number and Security Trailer and forward the remaining packet data with the verification result recorded in the frame context. | Inspection, Integration Test | +| AUTH007 | The component shall provide a command and telemetry channel to report the current sequence number to enable ground station synchronization. | Inspection, Integration Test | + +Opcode-based bypass policy (formerly AUTH002) is owned by ProvesRouter; see its SDD. + +## Change Log + +| Date | Description | +| --- | --- | +| 2025-11-26 | Initial design. | +| 2026-07-17 | Renamed to TcSecurityDeframer, refactor to discrete responsibilities: Authenticator, Parser, Validator. Pass-through interface between TcDeframer and SpacePacketDeframer; verification result carried in frame context; policy enforcement moved to ProvesRouter. | +| 2026-08-29 | Renamed to TcSecurityDecryptor and moved onto the upstream F Prime 4.3.0 SDLS hook: implements `Svc.Ccsds.CcsdsSdlsDecrypt` behind `Svc::Ccsds::CcsdsSdlsDeframer` instead of owning deframing/SA-extraction directly. The SA index is now a port argument rather than a parsed field; the MAC covers the SA index via the PSA multipart API. No `SdlsSaRouter` or `SdlsFileKeyManager` adopted (see "Relationship to CcsdsSdlsDeframer" above). | diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.hpp b/PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.hpp deleted file mode 100644 index ee16cda7..00000000 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.hpp +++ /dev/null @@ -1,32 +0,0 @@ -// ====================================================================== -// \title Validator.hpp -// \brief hpp file for packet policy validation helper functions -// ====================================================================== - -#pragma once - -#include -#include - -#include "Types.hpp" - -namespace Components { -namespace PacketValidator { - -//! Status of validation attempt -enum class Status { - Valid, //!< The packet is valid and passes all checks - SpiInvalid, //!< The packet SPI field is invalid - SequenceNumberInvalid, //!< The packet sequence number is outside the acceptable window -}; - -} // namespace PacketValidator - -//! Validate the packet against ruleset -PacketValidator::Status validatePacket( - const Ccsds355_0_B_2::TCSecurityHeader& secHeader, //!< The parsed security header - uint32_t sequenceNumber, //!< The current sequence number - uint32_t sequenceNumberWindow //!< The acceptable sequence number window -); - -} // namespace Components diff --git a/PROVESFlightControllerReference/Components/TcSecurityDeframer/docs/sdd.md b/PROVESFlightControllerReference/Components/TcSecurityDeframer/docs/sdd.md deleted file mode 100644 index cbdc9687..00000000 --- a/PROVESFlightControllerReference/Components/TcSecurityDeframer/docs/sdd.md +++ /dev/null @@ -1,209 +0,0 @@ -# Components::TcSecurityDeframer - -The TcSecurityDeframer component implements the TC ProcessSecurity flow of CCSDS 355.0-B-2 in the uplink path. It sits between TcDeframer and SpacePacketDeframer: it parses the Security Header and Trailer, validates the SPI and anti-replay sequence number, verifies the HMAC, then strips the security envelope and forwards the frame with the verification result recorded in the frame context (`authenticated` flag). - -The component does not enforce policy. Frames that fail verification are still forwarded (unauthenticated) so that downstream policy โ€” owned by ProvesRouter and its opcode bypass allowlist โ€” can decide whether to route or reject them. This keeps knowledge of packet structure here and knowledge of policy at the edge. - -## Overview - -The component is a thin stateful shell over pure-function namespaces: - -- `Ccsds355_0_B_2::parse` (Parser) โ€” Security Header (SPI, sequence number) and Trailer (MAC) extraction -- `Components::validatePacket` (Validator) โ€” SPI validation and anti-replay sequence-number window validation -- `Components::authenticatePacket` / `importHmacKey` (Authenticator) โ€” HMAC-SHA-256 (truncated to 16 bytes) verification via PSA crypto - -The only component state is the last accepted sequence number (mutex-guarded, persisted to file) and the imported HMAC key id. - -Primary data path connections: - -- TcDeframer.dataOut -> TcSecurityDeframer.dataIn -- TcSecurityDeframer.dataOut -> SpacePacketDeframer.dataIn -- TcSecurityDeframer.dataReturnOut -> TcDeframer.dataReturnIn -- SpacePacketDeframer.dataReturnOut -> TcSecurityDeframer.dataReturnIn - -## Class Diagram - -```mermaid -classDiagram -direction LR - -class TcSecurityDeframer { - +configure() - -dataIn_handler(portNum, data, context) - -dataReturnIn_handler(portNum, data, context) - -GET_SEQ_NUM_cmdHandler(opCode, cmdSeq) - -SET_SEQ_NUM_cmdHandler(opCode, cmdSeq, seqNum) - -readSequenceNumber(value) - -writeSequenceNumber(value) - -m_sequenceNumber : U32 - -m_sequenceNumberWindow : U32 - -m_hmacKeyId : uint32_t -} - -class Ccsds355_0_B_2 { - <> - +parse(buffer, size) Parser_Result -} - -class PacketValidator { - <> - +validatePacket(secHeader, sequenceNumber, window) Status -} - -class PacketAuthenticator { - <> - +importHmacKey(key, keyId) KeyImportResult - +authenticatePacket(buffer, size, mac, keyId) AuthenticationResult -} - -class TCSecurityHeader { - +spi : uint32_t - +sequenceNumber : uint32_t -} - -class TCSecurityTrailer { - +mac : Mac -} - -class Mac { - <> - +std::array~uint8_t,16~ -} - -TcSecurityDeframer ..> Ccsds355_0_B_2 : parses -TcSecurityDeframer ..> PacketValidator : validates -TcSecurityDeframer ..> PacketAuthenticator : authenticates -Ccsds355_0_B_2 --> TCSecurityHeader : returns -Ccsds355_0_B_2 --> TCSecurityTrailer : returns -TCSecurityTrailer --> Mac : contains -``` - -## Packet Format - -TcDeframer strips the TC Primary Header and FECF before this component, so the buffer received on dataIn is: - -- Security Header (6 bytes): SPI (2) + Sequence Number (4) -- Space Packet Primary Header (6 bytes) -- Space Packet Data Field (includes F Prime command) -- Security Trailer (16-byte MAC) - -Output packet layout (forwarded per CCSDS 355.0-B-2 ยง3.3.3.3): - -- Space Packet Primary Header (6 bytes) -- Space Packet Data Field - -The MAC is HMAC-SHA-256 truncated to 16 bytes, computed over the Security Header and Data Field (everything except the Security Trailer). - -### Additional resources - -- [CCSDS 355.0-B-2 Space Data Link Security Protocol](https://ccsds.org/Pubs/355x0b2.pdf) - -## Behavior - -1. Parse the Security Header and Trailer. If the frame is too short to contain them it cannot be stripped for downstream deframing: log ParsingFailed and return the buffer upstream (drop). -2. Validate the SPI (only SPI 0 is currently supported) and the anti-replay sequence number (must be strictly ahead of the last accepted value, within SEQ_NUM_WINDOW, with U32 wraparound handled). -3. If validation passes, verify the MAC. -4. Only when all checks pass: store and persist the received sequence number, telemeter it, and set `authenticated = true` in the frame context. Frames failing any check never advance the sequence number (issue #426). -5. Strip the Security Header and Trailer and forward on dataOut with the resulting `authenticated` flag. ProvesRouter rejects unauthenticated packets unless their opcode is on the bypass allowlist. - -At startup, `configure()` loads the persisted sequence number and telemeters it so the first downlinked value is correct before any command is accepted (issue #427). - -## Parameters - -| Name | Type | Default | Description | -|---|---|---|---| -| SEQ_NUM_WINDOW | U32 | 50000 | Maximum allowed forward sequence-number distance before rejecting a packet as out-of-window. | -| SEQ_NUM_FILE_PATH | string | "//sequence_number.txt" | File path used to persist and restore the sequence number across restarts. | - -## Port Descriptions - -| Name | Direction | Type | Description | -|---|---|---|---| -| dataIn | Input (guarded) | Svc.ComDataWithContext | Receives frames from TcDeframer for parse, validation, and authentication. | -| dataReturnIn | Input (sync) | Svc.ComDataWithContext | Receives returned ownership for buffers previously sent through dataOut. | -| dataOut | Output | Svc.ComDataWithContext | Forwards the stripped frame downstream with the authenticated flag set in the context. | -| dataReturnOut | Output | Svc.ComDataWithContext | Returns ownership of structurally invalid frames (and relays dataReturnIn ownership upstream). | - -Standard AC ports are also present for command handling, events, telemetry, parameter access, and time. - -## Telemetry Channels - -| Name | Type | Description | -|---|---|---| -| CurrentSequenceNumber | U32 | Current accepted sequence number tracked by the component. Emitted at startup and on each accepted packet. | - -Routed/bypassed/rejected packet counts are telemetered by ProvesRouter, which owns the accept/reject policy. - -## Events - -| Name | Severity | Parameters | Description | -|---|---|---|---| -| SequenceNumberGet | Activity High | seq_num: U32 | Logged by GET_SEQ_NUM on successful read. Format: "Sequence number is {}" | -| SequenceNumberReadFailed | Warning High (throttle 2) | status: Os.FileStatus | Logged when sequence-number read fails. Format: "Failed to read sequence number, error: {}" | -| SequenceNumberSet | Activity High | seq_num: U32 | Logged by SET_SEQ_NUM on successful write. Format: "Sequence number set to {}" | -| SequenceNumberWriteFailed | Warning High (throttle 2) | status: Os.FileStatus | Logged when sequence-number write fails. Format: "Failed to write sequence number, error: {}" | -| SequenceNumberInvalid | Warning High (throttle 2) | packet_seq_num: U32, seq_num: U32, window: U32 | Logged when anti-replay validation fails. Format: "Sequence number less than last accepted or out of window: Received={}, LastAccepted={}, Window={}" | -| AuthenticationFailed | Warning High (throttle 2) | auth_status: PacketAuthenticatorStatus, rc: I32 | Logged when MAC verification fails. Format: "Authentication failed: Status={}, PSA Return Code={}" | -| ParsingFailed | Warning High (throttle 2) | parse_status: PacketParserStatus | Logged when frame parsing fails. Format: "Parsing failed: {}" | -| SpiInvalid | Warning High (throttle 2) | packet_spi: U32 | Logged when SPI validation fails. Format: "SPI invalid: Received={}" | - -## Commands - -| Name | Type | Parameters | Description | -|---|---|---|---| -| GET_SEQ_NUM | Sync | None | Reads and reports the current sequence number (SequenceNumberGet event). | -| SET_SEQ_NUM | Sync | seq_num: U32 | Sets and persists a new sequence number (SequenceNumberSet event). | - -## Unit Tests - -TcSecurityDeframer helper functionality is covered by unit tests in PROVESFlightControllerReference/test/unit-tests: - -| Test File | Coverage | -|---|---| -| test_TcSecurityDeframer_Parser.cpp | Valid parse path plus parse failures for SPI, sequence number, and MAC size checks. | -| test_TcSecurityDeframer_Validator.cpp | SPI validation, out-of-window and replayed sequence numbers, window boundary, and wraparound handling. | -| test_TcSecurityDeframer_Authenticator.cpp | Key import failures, successful MAC verification, and failed verification with corrupted MAC or data. | - -Run unit tests with: - -```bash -make test-unit -``` - -## GDS Plugin - -To send authenticated packets from GDS, build the framing plugin: - -```bash -make framer-plugin -``` - -Then run GDS with the framing plugin enabled as configured by the project tooling. - -## Generating Keys - -The default authentication key header (AuthDefaultKey.h) is generated at build time from project key material via `make generate-auth-key` or `make copy-secrets`. This generated file is machine-local and not committed. - -## Requirements - -| Name | Description | Validation | -|---|---|---| -| AUTH001 | The component shall parse incoming frames to extract the SPI, sequence number, and MAC fields. | Unit Test | -| AUTH003 | The component shall validate that the SPI value corresponds to a configured Security Association. | Unit Test | -| AUTH004 | The component shall validate the received sequence number against the stored sequence number. | Unit Test | -| AUTH004-A | The component shall not authenticate packets with sequence numbers that are outside the acceptable window and shall log an event. | Unit Test, Inspection | -| AUTH004-B | The component shall set the stored sequence number to the sequence number transmitted in the packet only when a packet is fully validated and authenticated. | Inspection | -| AUTH004-C | The component shall allow the sequence number window to be configurable via a parameter. | Inspection | -| AUTH005 | The component shall compute the MAC over the entire frame minus the last 16-byte security trailer. | Unit Test | -| AUTH005-A | The component shall not mark packets as authenticated where the computed MAC does not match the security trailer MAC. | Unit Test | -| AUTH006 | For any parseable frame, the component shall remove the Security Header and Security Trailer and forward the remaining packet data with the verification result recorded in the frame context. | Inspection, Integration Test | -| AUTH007 | The component shall provide a command and telemetry channel to report the current sequence number to enable ground station synchronization. | Inspection, Integration Test | - -Opcode-based bypass policy (formerly AUTH002) is owned by ProvesRouter; see its SDD. - -## Change Log - -| Date | Description | -| --- | --- | -| 2025-11-26 | Initial design. | -| 2026-07-17 | Renamed to TcSecurityDeframer, refactor to discrete responsibilities: Authenticator, Parser, Validator. Pass-through interface between TcDeframer and SpacePacketDeframer; verification result carried in frame context; policy enforcement moved to ProvesRouter. | diff --git a/PROVESFlightControllerReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi b/PROVESFlightControllerReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi index bc25df67..6cc7bbfe 100644 --- a/PROVESFlightControllerReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi +++ b/PROVESFlightControllerReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi @@ -25,9 +25,9 @@ telemetry packets ReferenceDeploymentPackets { powerMonitor.TotalPowerGenerated # Communications Data - # ComCcsdsSband.tcSecurityDeframer.CurrentSequenceNumber - ComCcsdsLora.tcSecurityDeframer.CurrentSequenceNumber - ComCcsdsUart.tcSecurityDeframer.CurrentSequenceNumber + # ComCcsdsSband.tcSecurityDecryptor.CurrentSequenceNumber + ComCcsdsLora.tcSecurityDecryptor.CurrentSequenceNumber + ComCcsdsUart.tcSecurityDecryptor.CurrentSequenceNumber lora.BytesReceived diff --git a/PROVESFlightControllerReference/test/int/conftest.py b/PROVESFlightControllerReference/test/int/conftest.py index 933ebcee..3d9c219c 100644 --- a/PROVESFlightControllerReference/test/int/conftest.py +++ b/PROVESFlightControllerReference/test/int/conftest.py @@ -78,7 +78,7 @@ def pytest_addoption(parser: pytest.Parser) -> None: "--sync-deframer", choices=["uart", "lora"], default=None, - help="Which TcSecurityDeframer instance the sequence-number sync test " + help="Which TcSecurityDecryptor instance the sequence-number sync test " "queries. Defaults to lora when --with-radio is set, uart otherwise. " "The radio CI job's UART-side bootstrap passes --sync-deframer=lora " "because the traffic that follows is validated by the LoRa instance.", diff --git a/PROVESFlightControllerReference/test/int/sync_sequence_number_test.py b/PROVESFlightControllerReference/test/int/sync_sequence_number_test.py index 014e21f3..ad9aee28 100644 --- a/PROVESFlightControllerReference/test/int/sync_sequence_number_test.py +++ b/PROVESFlightControllerReference/test/int/sync_sequence_number_test.py @@ -30,8 +30,8 @@ def test_sync_sequence_number( else "uart" ) deframer = { - "uart": "ComCcsdsUart.tcSecurityDeframer", - "lora": "ComCcsdsLora.tcSecurityDeframer", + "uart": "ComCcsdsUart.tcSecurityDecryptor", + "lora": "ComCcsdsLora.tcSecurityDecryptor", }[link] proves_send_and_assert_command(fprime_test_api, f"{deframer}.GET_SEQ_NUM") evt: EventData = fprime_test_api.assert_event( diff --git a/PROVESFlightControllerReference/test/unit-tests/CMakeLists.txt b/PROVESFlightControllerReference/test/unit-tests/CMakeLists.txt index 389ccaf6..a708bfcd 100644 --- a/PROVESFlightControllerReference/test/unit-tests/CMakeLists.txt +++ b/PROVESFlightControllerReference/test/unit-tests/CMakeLists.txt @@ -31,27 +31,27 @@ target_include_directories(detumble_manager_strategy_selector PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../.. ) -# TcSecurityDeframer Parser -add_library(security_deframer_parser STATIC - ${CMAKE_CURRENT_SOURCE_DIR}/../../../PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.cpp +# TcSecurityDecryptor Parser +add_library(security_decryptor_parser STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/../../../PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.cpp ) -target_include_directories(security_deframer_parser PUBLIC +target_include_directories(security_decryptor_parser PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../.. ) -# TcSecurityDeframer Validator -add_library(security_deframer_validator STATIC - ${CMAKE_CURRENT_SOURCE_DIR}/../../../PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.cpp +# TcSecurityDecryptor Validator +add_library(security_decryptor_validator STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/../../../PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.cpp ) -target_include_directories(security_deframer_validator PUBLIC +target_include_directories(security_decryptor_validator PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../.. ) -# TcSecurityDeframer Authenticator -add_library(security_deframer_authenticator STATIC - ${CMAKE_CURRENT_SOURCE_DIR}/../../../PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.cpp +# TcSecurityDecryptor Authenticator +add_library(security_decryptor_authenticator STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/../../../PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.cpp ) -target_include_directories(security_deframer_authenticator PUBLIC +target_include_directories(security_decryptor_authenticator PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../.. ${CMAKE_CURRENT_SOURCE_DIR} # for AuthDefaultKey.h ) @@ -84,8 +84,8 @@ if(NOT MBEDCRYPTO_LIB) message(FATAL_ERROR "mbedcrypto library not found; install mbedtls (brew install mbedtls) or provide library in LIBRARY_PATH") endif() -target_include_directories(security_deframer_authenticator PUBLIC ${PSA_CRYPTO_H}) -target_link_libraries(security_deframer_authenticator PUBLIC ${MBEDCRYPTO_LIB}) +target_include_directories(security_decryptor_authenticator PUBLIC ${PSA_CRYPTO_H}) +target_link_libraries(security_decryptor_authenticator PUBLIC ${MBEDCRYPTO_LIB}) # --- Auto-discover and build tests --- @@ -100,9 +100,9 @@ foreach(test_src ${TEST_SOURCES}) detumble_manager_bdot detumble_manager_magnetorquer detumble_manager_strategy_selector - security_deframer_parser - security_deframer_validator - security_deframer_authenticator + security_decryptor_parser + security_decryptor_validator + security_decryptor_authenticator rtc_manager_rtc_helper proves_router_bypasser ) diff --git a/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Authenticator.cpp b/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Authenticator.cpp similarity index 60% rename from PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Authenticator.cpp rename to PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Authenticator.cpp index dbcb5e8c..fa2576f6 100644 --- a/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Authenticator.cpp +++ b/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Authenticator.cpp @@ -3,17 +3,22 @@ #include -#include "PROVESFlightControllerReference/Components/TcSecurityDeframer/Authenticator.hpp" +#include "PROVESFlightControllerReference/Components/TcSecurityDecryptor/Authenticator.hpp" using namespace Components; constexpr char kTestKeyHex[] = "14408c2711281f4d70452ce3730bb4fa"; //!< The hex-encoded key corresponding to the MAC in the test packets -//! 16 data bytes followed by their HMAC-SHA-256 MAC truncated to 16 bytes, computed with kTestKeyHex +//! 16 data bytes followed by their HMAC-SHA-256 MAC (truncated to 16 bytes) computed with kTestKeyHex +//! over [SA index 0 (2 bytes, big-endian) | data] static const std::vector kTestPacket = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, - 12, 13, 14, 15, 16, 0x54, 0x92, 0x46, 0xAF, 0xF2, 0xEA, - 0x86, 0x7C, 0xEB, 0xBC, 0x38, 0x5D, 0x73, 0xF8, 0x94, 0x9C}; + 12, 13, 14, 15, 16, 0x8C, 0xA5, 0x91, 0x0D, 0xB6, 0xE0, + 0xC1, 0xEA, 0x6F, 0x06, 0x05, 0x0C, 0x37, 0x89, 0xBC, 0xD9}; + +//! The same 16 data bytes' HMAC-SHA-256 MAC (truncated to 16 bytes) computed under SA index 1 +static const Mac kTestMacUnderSa1 = {0x84, 0xE9, 0xF4, 0xCB, 0xC4, 0xFD, 0x37, 0x5B, + 0x66, 0xD3, 0x95, 0x7F, 0xE8, 0xD6, 0xD2, 0x7B}; //! Import the test key, asserting success, and return the PSA key id static uint32_t importTestKey() { @@ -48,18 +53,36 @@ TEST(PacketAuthenticatorTest, ImportNullKey) { TEST(PacketAuthenticatorTest, NullBuffer) { uint32_t keyId = importTestKey(); Mac mac{}; - auto res = authenticatePacket(nullptr, 0, mac, keyId); + auto res = authenticateFrame(0, nullptr, 0, mac, keyId); EXPECT_EQ(res.status, PacketAuthenticator::AuthenticationStatus::VerifyError); EXPECT_EQ(res.psaStatus, PSA_ERROR_INVALID_ARGUMENT); } TEST(PacketAuthenticatorTest, AuthenticatedSuccess) { uint32_t keyId = importTestKey(); - auto res = authenticatePacket(kTestPacket.data(), kTestPacket.size(), macOf(kTestPacket), keyId); + auto res = authenticateFrame(0, kTestPacket.data(), kTestPacket.size(), macOf(kTestPacket), keyId); EXPECT_EQ(res.status, PacketAuthenticator::AuthenticationStatus::Authenticated); EXPECT_EQ(res.psaStatus, PSA_SUCCESS); } +TEST(PacketAuthenticatorTest, SaIndexParticipatesInMac) { + // The same [data|MAC] bytes verify under SA 0 but fail under SA 1, and the MAC computed + // for SA 1 fails under SA 0: the SA index is not just a routing key, it is authenticated. + uint32_t keyId = importTestKey(); + + auto resUnderSa1 = authenticateFrame(1, kTestPacket.data(), kTestPacket.size(), macOf(kTestPacket), keyId); + EXPECT_EQ(resUnderSa1.status, PacketAuthenticator::AuthenticationStatus::VerifyError); + + // authenticateFrame authenticates size-Mac::size() bytes of the buffer, so the buffer must + // still be full-length (data + trailer space) even though the trailing bytes are unused. + std::vector data = kTestPacket; + auto resSa1MacUnderSa1 = authenticateFrame(1, data.data(), data.size(), kTestMacUnderSa1, keyId); + EXPECT_EQ(resSa1MacUnderSa1.status, PacketAuthenticator::AuthenticationStatus::Authenticated); + + auto resSa1MacUnderSa0 = authenticateFrame(0, data.data(), data.size(), kTestMacUnderSa1, keyId); + EXPECT_EQ(resSa1MacUnderSa0.status, PacketAuthenticator::AuthenticationStatus::VerifyError); +} + TEST(PacketAuthenticatorTest, VerifyFailure) { uint32_t keyId = importTestKey(); Mac mac = macOf(kTestPacket); @@ -67,7 +90,7 @@ TEST(PacketAuthenticatorTest, VerifyFailure) { // Corrupt one byte of the MAC mac[0] ^= 0xFF; - auto res = authenticatePacket(kTestPacket.data(), kTestPacket.size(), mac, keyId); + auto res = authenticateFrame(0, kTestPacket.data(), kTestPacket.size(), mac, keyId); EXPECT_EQ(res.status, PacketAuthenticator::AuthenticationStatus::VerifyError); EXPECT_NE(res.psaStatus, PSA_SUCCESS); } @@ -79,7 +102,7 @@ TEST(PacketAuthenticatorTest, CorruptedDataFails) { // Corrupt one authenticated data byte packet[0] ^= 0xFF; - auto res = authenticatePacket(packet.data(), packet.size(), macOf(packet), keyId); + auto res = authenticateFrame(0, packet.data(), packet.size(), macOf(packet), keyId); EXPECT_EQ(res.status, PacketAuthenticator::AuthenticationStatus::VerifyError); EXPECT_NE(res.psaStatus, PSA_SUCCESS); } @@ -89,7 +112,7 @@ TEST(PacketAuthenticatorTest, ShortBuffer) { std::vector packet = {1, 2, 3}; // Too short to contain a MAC Mac mac{}; - auto res = authenticatePacket(packet.data(), packet.size(), mac, keyId); + auto res = authenticateFrame(0, packet.data(), packet.size(), mac, keyId); EXPECT_EQ(res.status, PacketAuthenticator::AuthenticationStatus::VerifyError); EXPECT_EQ(res.psaStatus, PSA_ERROR_INVALID_ARGUMENT); } @@ -101,7 +124,7 @@ TEST(PacketAuthenticatorTest, MinimumSizeBuffer) { std::vector packet(Ccsds355_0_B_2::kTCSecurityTrailer, 0); Mac mac{}; - auto res = authenticatePacket(packet.data(), packet.size(), mac, keyId); + auto res = authenticateFrame(0, packet.data(), packet.size(), mac, keyId); EXPECT_EQ(res.status, PacketAuthenticator::AuthenticationStatus::VerifyError); EXPECT_EQ(res.psaStatus, PSA_ERROR_INVALID_SIGNATURE); } diff --git a/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Parser.cpp b/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Parser.cpp similarity index 51% rename from PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Parser.cpp rename to PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Parser.cpp index aa0bfe05..626dfc88 100644 --- a/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Parser.cpp +++ b/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Parser.cpp @@ -2,83 +2,66 @@ #include -#include "PROVESFlightControllerReference/Components/TcSecurityDeframer/Parser.hpp" +#include "PROVESFlightControllerReference/Components/TcSecurityDecryptor/Parser.hpp" using namespace Components; using Parser = Ccsds355_0_B_2::TcTransferFrame::Parser::Status; TEST(PacketParserTest, SuccessfulParse) { - // Security header (6) + data field (12) + security trailer (16) = 34 - std::vector buf(34, 0); - - // SPI (2 bytes) - buf[0] = 0x12; - buf[1] = 0x34; // SPI = 0x1234 + // Sequence number (4) + data field (12) + security trailer (16) = 32 + std::vector buf(32, 0); // Sequence number (4 bytes big-endian) - buf[2] = 0x01; - buf[3] = 0x02; - buf[4] = 0x03; - buf[5] = 0x04; // seq = 0x01020304 + buf[0] = 0x01; + buf[1] = 0x02; + buf[2] = 0x03; + buf[3] = 0x04; // seq = 0x01020304 - // MAC occupies last 16 bytes (indices 18..33) + // MAC occupies last 16 bytes (indices 16..31) for (size_t i = 0; i < 16; ++i) { - buf[18 + i] = static_cast(i); + buf[16 + i] = static_cast(i); } auto res = Ccsds355_0_B_2::parse(buf.data(), buf.size()); EXPECT_EQ(res.status, Parser::Ok); - EXPECT_EQ(res.securityHeader.spi, 0x1234u); EXPECT_EQ(res.securityHeader.sequenceNumber, 0x01020304u); for (size_t i = 0; i < 16; ++i) { EXPECT_EQ(res.securityTrailer.mac[i], static_cast(i)); } - // Frame data is everything between the security header and trailer - EXPECT_EQ(res.frameData.data, buf.data() + Ccsds355_0_B_2::kTCSecurityHeaderSize); + // Frame data is everything between the sequence number and the trailer + EXPECT_EQ(res.frameData.data, buf.data() + Ccsds355_0_B_2::kSequenceNumberSize); EXPECT_EQ(res.frameData.size, - buf.size() - Ccsds355_0_B_2::kTCSecurityHeaderSize - Ccsds355_0_B_2::kTCSecurityTrailer); + buf.size() - Ccsds355_0_B_2::kSequenceNumberSize - Ccsds355_0_B_2::kTCSecurityTrailer); } TEST(PacketParserTest, NullBuffer) { - auto res = Ccsds355_0_B_2::parse(nullptr, 34); - EXPECT_EQ(res.status, Parser::SpiParseError); -} - -TEST(PacketParserTest, SpiParseErrorTooSmall) { - std::vector buf(1, 0); // smaller than SPI field requirement (2) - - auto res = Ccsds355_0_B_2::parse(buf.data(), buf.size()); - EXPECT_EQ(res.status, Parser::SpiParseError); + auto res = Ccsds355_0_B_2::parse(nullptr, 32); + EXPECT_EQ(res.status, Parser::SequenceNumberParseError); } TEST(PacketParserTest, SequenceNumberParseErrorTooShort) { - // Large enough for SPI (2 bytes) but too short for the full security header (6) + // Too short for the full sequence number (4) std::vector buf(3, 0); - buf[0] = 0x00; - buf[1] = 0x01; // SPI present - auto res = Ccsds355_0_B_2::parse(buf.data(), buf.size()); EXPECT_EQ(res.status, Parser::SequenceNumberParseError); } TEST(PacketParserTest, MacParseErrorTooShort) { - // Large enough for the security header (6) but too short to contain the MAC (needs 22) - std::vector buf(20, 0); + // Large enough for the sequence number (4) but too short to contain the MAC (needs 20) + std::vector buf(10, 0); - buf[0] = 0x00; - buf[1] = 0x01; - buf[5] = 0x02; + buf[3] = 0x02; auto res = Ccsds355_0_B_2::parse(buf.data(), buf.size()); EXPECT_EQ(res.status, Parser::MacParseError); } TEST(PacketParserTest, MinimumSizeEmptyDataField) { - // Exactly header + trailer: parses with an empty data field + // Exactly SeqNum + trailer: parses with an empty data field std::vector buf(Ccsds355_0_B_2::kMinAuthenticatedPacketSize, 0); auto res = Ccsds355_0_B_2::parse(buf.data(), buf.size()); diff --git a/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Validator.cpp b/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Validator.cpp similarity index 63% rename from PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Validator.cpp rename to PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Validator.cpp index 998276d3..6e5f0418 100644 --- a/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDeframer_Validator.cpp +++ b/PROVESFlightControllerReference/test/unit-tests/test_TcSecurityDecryptor_Validator.cpp @@ -1,31 +1,30 @@ #include -#include "PROVESFlightControllerReference/Components/TcSecurityDeframer/Validator.hpp" +#include "PROVESFlightControllerReference/Components/TcSecurityDecryptor/Validator.hpp" using namespace Components; -using Header = Ccsds355_0_B_2::TCSecurityHeader; TEST(PacketValidatorTest, ValidPacket) { - Header h{0u, 11u}; // spi 0 valid, seq 11 within window of expected 10 - auto res = validatePacket(h, 10u, 5u); + // sa index 0 valid, seq 11 within window of expected 10 + auto res = validateFrame(0u, 11u, 10u, 5u); EXPECT_EQ(res, PacketValidator::Status::Valid); } TEST(PacketValidatorTest, SpiInvalid) { - Header h{1u, 11u}; // non-zero SPI invalid - auto res = validatePacket(h, 10u, 5u); + // non-zero SA index invalid + auto res = validateFrame(1u, 11u, 10u, 5u); EXPECT_EQ(res, PacketValidator::Status::SpiInvalid); } TEST(PacketValidatorTest, SequenceNumberInvalid) { - Header h{0u, 20u}; // sequence 20, expected 10, window 5 -> out - auto res = validatePacket(h, 10u, 5u); + // sequence 20, expected 10, window 5 -> out + auto res = validateFrame(0u, 20u, 10u, 5u); EXPECT_EQ(res, PacketValidator::Status::SequenceNumberInvalid); } TEST(PacketValidatorTest, SequenceNumberReplayRejected) { - Header h{0u, 9u}; // below last accepted -> replay - auto res = validatePacket(h, 10u, 5u); + // below last accepted -> replay + auto res = validateFrame(0u, 9u, 10u, 5u); EXPECT_EQ(res, PacketValidator::Status::SequenceNumberInvalid); } @@ -34,9 +33,7 @@ TEST(PacketValidatorTest, SequenceNumberWrapWithinWindow) { const uint32_t expected = 0xFFFFFFFEu; // -2 const uint32_t seq = 1u; // wrapped value // distance = seq - expected = 1 - 0xFFFFFFFE = 3 (mod 2^32) - Header h{0u, seq}; - - auto res = validatePacket(h, expected, 5u); + auto res = validateFrame(0u, seq, expected, 5u); EXPECT_EQ(res, PacketValidator::Status::Valid); } @@ -45,20 +42,18 @@ TEST(PacketValidatorTest, SequenceNumberWrapOutOfWindow) { const uint32_t expected = 0xFFFFFFF0u; // -16 const uint32_t seq = 10u; // wrapped value // distance = 10 - 0xFFFFFFF0 = 26 (mod 2^32) - Header h{0u, seq}; - - auto res = validatePacket(h, expected, 5u); + auto res = validateFrame(0u, seq, expected, 5u); EXPECT_EQ(res, PacketValidator::Status::SequenceNumberInvalid); } TEST(PacketValidatorTest, SequenceNumberEqualToExpected) { - Header h{0u, 10u}; // sequence equal to last accepted must be rejected (no reuse) - auto res = validatePacket(h, 10u, 5u); + // sequence equal to last accepted must be rejected (no reuse) + auto res = validateFrame(0u, 10u, 10u, 5u); EXPECT_EQ(res, PacketValidator::Status::SequenceNumberInvalid); } TEST(PacketValidatorTest, SequenceNumberAtWindowBoundary) { - Header h{0u, 15u}; // expected 10, window 5 -> distance = 5 = window -> valid - auto res = validatePacket(h, 10u, 5u); + // expected 10, window 5 -> distance = 5 = window -> valid + auto res = validateFrame(0u, 15u, 10u, 5u); EXPECT_EQ(res, PacketValidator::Status::Valid); } diff --git a/README.md b/README.md index 1ad4c26e..c41b2976 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ MCUBoot only boots images that are **signed with the same key** the bootloader i If you regenerate/replace the bootloader (or switch computers and flash a bootloader built elsewhere), make sure you also update `keys/proves.pem` to the matching signing key, or your built images will not boot. -You also want to make sure the authentication key the gds runs with is the same as the authentication key on the board. For that, you want to make sure the authentication key in PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h matches. +You also want to make sure the authentication key the gds runs with is the same as the authentication key on the board. For that, you want to make sure the authentication key in PROVESFlightControllerReference/Components/TcSecurityDecryptor/AuthDefaultKey.h matches. ## Running Integration Tests diff --git a/docs-site/components/ProvesRouter.md b/docs-site/components/ProvesRouter.md index 68408392..8ba4446d 100644 --- a/docs-site/components/ProvesRouter.md +++ b/docs-site/components/ProvesRouter.md @@ -10,13 +10,13 @@ The `Svc::ProvesRouter` component supports `Fw::ComPacketType::FW_PACKET_COMMAND ## Security Policy Enforcement -`Svc::ProvesRouter` is the policy point of the uplink security design. Upstream, `Components::TcSecurityDeframer` verifies each frame (SPI, anti-replay sequence number, MAC) and records the result in the `ComCfg::FrameContext` `authenticated` flag without dropping anything. The router then enforces: +`Svc::ProvesRouter` is the policy point of the uplink security design. Upstream, `Components::TcSecurityDecryptor` (a decryptor behind the upstream `Svc::Ccsds::CcsdsSdlsDeframer`) verifies each frame (security association index, anti-replay sequence number, MAC) and records the result in the `ComCfg::FrameContext` `authenticated` flag without dropping anything. The router then enforces: - Packets with `authenticated == true` are routed normally. - Unauthenticated packets are routed only if their opcode is on the hardcoded bypass allowlist (`Components::PacketBypasser::bypassPacket` in `Bypasser.cpp`), which permits public commands such as `CMD_NO_OP`, `GET_SEQ_NUM`, and `TELL_JOKE`. - All other unauthenticated packets are rejected: ownership is returned via `dataReturnOut` and the packet is not routed. -Because bypassed packets never pass through the authenticated-accept path in TcSecurityDeframer, they cannot advance the anti-replay sequence number. +Because bypassed packets never pass through the authenticated-accept path in TcSecurityDecryptor, they cannot advance the anti-replay sequence number. About memory management, all buffers sent by `Svc::ProvesRouter` on the `fileOut` and `unknownDataOut` ports are expected to be returned to the router through the `fileBufferReturnIn` port for deallocation. diff --git a/docs-site/components/TcSecurityDecryptor.md b/docs-site/components/TcSecurityDecryptor.md new file mode 100644 index 00000000..e3946f82 --- /dev/null +++ b/docs-site/components/TcSecurityDecryptor.md @@ -0,0 +1,280 @@ +# Components::TcSecurityDecryptor + +The TcSecurityDecryptor component implements the TC ProcessSecurity flow of CCSDS 355.0-B-2 in the +uplink path, acting as the decryptor behind the upstream F Prime `Svc::Ccsds::CcsdsSdlsDeframer` +(introduced in F Prime 4.3.0). The SDLS deframer extracts the 2-byte security association (SA) +index from the frame and calls this component via the `Svc.Ccsds.CcsdsSdlsDecrypt` interface; this +component validates the anti-replay sequence number, verifies the HMAC (which covers the SA index +and the frame data), then strips the remaining security envelope and forwards the frame with the +verification result recorded in the frame context (`authenticated` flag). + +The component does not enforce policy. Frames that fail verification are still forwarded +(unauthenticated) so that downstream policy โ€” owned by ProvesRouter and its opcode bypass +allowlist โ€” can decide whether to route or reject them. This keeps knowledge of packet structure +here and knowledge of policy at the edge. + +## Relationship to CcsdsSdlsDeframer + +This project previously owned the full deframing/SA-extraction step in a single component +(`TcSecurityDeframer`, a pass-through between `TcDeframer` and `SpacePacketDeframer`) because stock +F Prime had no security hook. F Prime 4.3.0 added that hook, so responsibility is now split: + +- `Svc::Ccsds::CcsdsSdlsDeframer` (upstream, unmodified): strips the 2-byte SA index from the frame + and calls the decryptor with the SA index and the remaining `[SeqNum | Data | MAC]` buffer. +- `Components::TcSecurityDecryptor` (this component): validates and authenticates using the SA + index supplied by the deframer, strips the sequence number and MAC, and reports the result. + +Two deliberate deviations from a "default" upstream integration: + +1. **No `Svc::Ccsds::SdlsSaRouter`.** The router selects a *component* by SA index; issue #472's + design selects a *key* by SA index inside this component's 2-slot key store. The router would + add a passive component and a tracking table per subtopology (three on the rp2350) for no + benefit today. `sdlsDeframer.decryptOut` connects directly to this component; inserting a router + later is a topology-only change if a project ever needs to dispatch to multiple decryptor + implementations by SA index. +2. **`SdlsStatus::SUCCESS` returned for every structurally parseable frame.** Upstream's + `CcsdsSdlsDeframer` drops any frame whose decrypt status is not `SUCCESS`, which would defeat the + #426 bypass path (unauthenticated `CMD_NO_OP` must still reach `ProvesRouter` so its allowlist can + accept it). This component therefore expresses authenticity solely via + `context.authenticated` and returns `DECRYPTION_FAILURE` only when the frame is too short to + parse, where dropping is correct regardless of authentication policy. This is a deliberate + reading of an authentication-only security association, not a bug: there is no separate notion + of "decryption failure" when the SA performs no decryption. + +### Why not `Svc::Ccsds::SdlsFileKeyManager` + +Upstream's `SdlsFileKeyManager` and the `Svc.Ccsds.SdlsKeyInterface` port were not adopted. The +upstream `SdlsKey` port carries no SA index, so it cannot express the 2-slot, SPI-keyed key store +that issue #472 needs, and #472/#490 put `PROVISION_KEY`/`ADD_KEY`/`REMOVE_KEY`/`GET_ACTIVE_KEYS` +commands directly on this component. Key material therefore stays inside this component's shell; +`SdlsFileKeyManager` would have to grow an SA-index argument on every port and duplicate the command +surface #472 already defines here, which is strictly more code for no capability gain. Adopting it +would become worth revisiting only if a future SA needed to share key material across multiple +security components. + +## Overview + +The component is a thin stateful shell over pure-function namespaces: + +- `Ccsds355_0_B_2::parse` (Parser) โ€” sequence number and Security Trailer (MAC) extraction from + `[SeqNum | Data | MAC]` (the SA index is not part of this buffer; it arrives as a port argument) +- `Components::validateFrame` (Validator) โ€” SA index validation and anti-replay sequence-number + window validation +- `Components::authenticateFrame` / `importHmacKey` (Authenticator) โ€” HMAC-SHA-256 (truncated to 16 + bytes) verification via the PSA multipart MAC API, covering the SA index followed by the frame + data + +The only component state is the last accepted sequence number (mutex-guarded, persisted to file) +and the imported HMAC key id. + +Primary data path connections (per uplink subtopology): + +- `sdlsDeframer.decryptOut -> tcSecurityDecryptor.decryptIn` +- `tcSecurityDecryptor.decryptOut -> sdlsDeframer.decryptIn` +- `sdlsDeframer.decryptReturnOut -> tcSecurityDecryptor.decryptReturnIn` +- `tcSecurityDecryptor.bufferReturnOut -> sdlsDeframer.bufferReturnIn` + +## Class Diagram + +```mermaid +classDiagram +direction LR + +class TcSecurityDecryptor { + +configure() + -decryptIn_handler(portNum, saIndex, data, context) + -decryptReturnIn_handler(portNum, data, context) + -GET_SEQ_NUM_cmdHandler(opCode, cmdSeq) + -SET_SEQ_NUM_cmdHandler(opCode, cmdSeq, seqNum) + -readSequenceNumber(value) + -writeSequenceNumber(value) + -m_sequenceNumber : U32 + -m_sequenceNumberWindow : U32 + -m_hmacKeyId : uint32_t +} + +class Ccsds355_0_B_2 { + <> + +parse(buffer, size) Parser_Result +} + +class PacketValidator { + <> + +validateFrame(saIndex, packetSeqNum, sequenceNumber, window) Status +} + +class PacketAuthenticator { + <> + +importHmacKey(key, keyId) KeyImportResult + +authenticateFrame(saIndex, buffer, size, mac, keyId) AuthenticationResult +} + +class TCSecurityHeader { + +sequenceNumber : uint32_t +} + +class TCSecurityTrailer { + +mac : Mac +} + +class Mac { + <> + +std::array~uint8_t,16~ +} + +TcSecurityDecryptor ..> Ccsds355_0_B_2 : parses +TcSecurityDecryptor ..> PacketValidator : validates +TcSecurityDecryptor ..> PacketAuthenticator : authenticates +Ccsds355_0_B_2 --> TCSecurityHeader : returns +Ccsds355_0_B_2 --> TCSecurityTrailer : returns +TCSecurityTrailer --> Mac : contains +``` + +## Packet Format + +The upstream `Svc::Ccsds::CcsdsSdlsDeframer` strips the SA index (2 bytes) before calling this +component, so the buffer received on `decryptIn` is: + +- Sequence Number (4 bytes) +- Space Packet Primary Header (6 bytes) +- Space Packet Data Field (includes F Prime command) +- Security Trailer (16-byte MAC) + +Output packet layout (forwarded per CCSDS 355.0-B-2 ยง3.3.3.3): + +- Space Packet Primary Header (6 bytes) +- Space Packet Data Field + +The MAC is HMAC-SHA-256 truncated to 16 bytes, computed over the SA index (2 bytes, big-endian) and +the Data Field (everything except the Security Trailer). Because the SA index is stripped from the +buffer by the deframer before this component sees it, verification uses the PSA multipart MAC API +(`psa_mac_verify_setup` โ†’ `update(saIndex)` โ†’ `update(data)` โ†’ `psa_mac_verify_finish`) rather than a +single `psa_mac_verify` call. The ground-side `Framing/src/authenticate_plugin.py` is unchanged: it +still emits `SPI(2) | SeqNum(4) | Data | MAC(16)` and MACs `SPI | SeqNum | Data`. + +### Additional resources + +- [CCSDS 355.0-B-2 Space Data Link Security Protocol](https://ccsds.org/Pubs/355x0b2.pdf) + +## Behavior + +1. Parse the sequence number and Security Trailer. If the frame is too short to contain them it + cannot be stripped for downstream deframing: log ParsingFailed and report + `SdlsStatus::DECRYPTION_FAILURE` on `decryptOut` (the upstream deframer drops the frame, notifies + `errorNotify`, and hands the buffer back). +2. Validate the SA index (only SA 0 is currently supported) and the anti-replay sequence number + (must be strictly ahead of the last accepted value, within SEQ_NUM_WINDOW, with U32 wraparound + handled). +3. If validation passes, verify the MAC. +4. Only when all checks pass: store and persist the received sequence number, telemeter it, and set + `authenticated = true` in the frame context. Frames failing any check never advance the sequence + number (issue #426). +5. Strip the sequence number and Security Trailer and forward on `decryptOut` with + `SdlsStatus::SUCCESS` and the resulting `authenticated` flag (see "Relationship to + CcsdsSdlsDeframer" above for why every parseable frame reports `SUCCESS`). ProvesRouter rejects + unauthenticated packets unless their opcode is on the bypass allowlist. + +At startup, `configure()` loads the persisted sequence number and telemeters it so the first +downlinked value is correct before any command is accepted (issue #427). + +## Parameters + +| Name | Type | Default | Description | +|---|---|---|---| +| SEQ_NUM_WINDOW | U32 | 50000 | Maximum allowed forward sequence-number distance before rejecting a packet as out-of-window. | +| SEQ_NUM_FILE_PATH | string | "//sequence_number.txt" | File path used to persist and restore the sequence number across restarts. | + +## Port Descriptions + +| Name | Direction | Type | Description | +|---|---|---|---| +| decryptIn | Input (guarded) | Svc.Ccsds.CcsdsSdlsEncryption | Receives the SA index and `[SeqNum \| Data \| MAC]` buffer from the upstream SDLS deframer. | +| decryptReturnIn | Input (guarded) | Svc.ComDataWithContext | Receives back ownership of buffers previously sent on decryptOut. | +| decryptOut | Output | Svc.Ccsds.CcsdsSdlsData | Forwards the decrypt status and stripped frame downstream with the authenticated flag set in the context. | +| bufferReturnOut | Output | Svc.ComDataWithContext | Returns ownership of the incoming iv/data buffer (relays decryptReturnIn ownership upstream to the deframer). | + +Standard AC ports are also present for command handling, events, telemetry, parameter access, and +time. + +## Telemetry Channels + +| Name | Type | Description | +|---|---|---| +| CurrentSequenceNumber | U32 | Current accepted sequence number tracked by the component. Emitted at startup and on each accepted packet. | + +Routed/bypassed/rejected packet counts are telemetered by ProvesRouter, which owns the accept/reject policy. + +## Events + +| Name | Severity | Parameters | Description | +|---|---|---|---| +| SequenceNumberGet | Activity High | seq_num: U32 | Logged by GET_SEQ_NUM on successful read. Format: "Sequence number is {}" | +| SequenceNumberReadFailed | Warning High (throttle 2) | status: Os.FileStatus | Logged when sequence-number read fails. Format: "Failed to read sequence number, error: {}" | +| SequenceNumberSet | Activity High | seq_num: U32 | Logged by SET_SEQ_NUM on successful write. Format: "Sequence number set to {}" | +| SequenceNumberWriteFailed | Warning High (throttle 2) | status: Os.FileStatus | Logged when sequence-number write fails. Format: "Failed to write sequence number, error: {}" | +| SequenceNumberInvalid | Warning High (throttle 2) | packet_seq_num: U32, seq_num: U32, window: U32 | Logged when anti-replay validation fails. Format: "Sequence number less than last accepted or out of window: Received={}, LastAccepted={}, Window={}" | +| AuthenticationFailed | Warning High (throttle 2) | auth_status: PacketAuthenticatorStatus, rc: I32 | Logged when MAC verification fails. Format: "Authentication failed: Status={}, PSA Return Code={}" | +| ParsingFailed | Warning High (throttle 2) | parse_status: PacketParserStatus | Logged when frame parsing fails. Format: "Parsing failed: {}" | +| SpiInvalid | Warning High (throttle 2) | sa_index: U32 | Logged when SA index validation fails. Format: "Security association index invalid: Received={}" | + +## Commands + +| Name | Type | Parameters | Description | +|---|---|---|---| +| GET_SEQ_NUM | Sync | None | Reads and reports the current sequence number (SequenceNumberGet event). | +| SET_SEQ_NUM | Sync | seq_num: U32 | Sets and persists a new sequence number (SequenceNumberSet event). | + +## Unit Tests + +TcSecurityDecryptor helper functionality is covered by unit tests in PROVESFlightControllerReference/test/unit-tests: + +| Test File | Coverage | +|---|---| +| test_TcSecurityDecryptor_Parser.cpp | Valid parse path plus parse failures for sequence number and MAC size checks. | +| test_TcSecurityDecryptor_Validator.cpp | SA index validation, out-of-window and replayed sequence numbers, window boundary, and wraparound handling. | +| test_TcSecurityDecryptor_Authenticator.cpp | Key import failures, successful MAC verification, failed verification with corrupted MAC or data, and coverage that the SA index participates in the MAC (the same `[data\|MAC]` bytes verify under SA 0 but fail under SA 1). | + +Run unit tests with: + +```bash +make test-unit +``` + +## GDS Plugin + +To send authenticated packets from GDS, build the framing plugin: + +```bash +make framer-plugin +``` + +Then run GDS with the framing plugin enabled as configured by the project tooling. + +## Generating Keys + +The default authentication key header (AuthDefaultKey.h) is generated at build time from project key material via `make generate-auth-key` or `make copy-secrets`. This generated file is machine-local and not committed. + +## Requirements + +| Name | Description | Validation | +|---|---|---| +| AUTH001 | The component shall parse incoming frames to extract the sequence number and MAC fields. | Unit Test | +| AUTH003 | The component shall validate that the security association index supplied by the upstream deframer corresponds to a configured Security Association. | Unit Test | +| AUTH004 | The component shall validate the received sequence number against the stored sequence number. | Unit Test | +| AUTH004-A | The component shall not authenticate packets with sequence numbers that are outside the acceptable window and shall log an event. | Unit Test, Inspection | +| AUTH004-B | The component shall set the stored sequence number to the sequence number transmitted in the packet only when a packet is fully validated and authenticated. | Inspection | +| AUTH004-C | The component shall allow the sequence number window to be configurable via a parameter. | Inspection | +| AUTH005 | The component shall compute the MAC over the security association index and the frame data minus the 16-byte security trailer. | Unit Test | +| AUTH005-A | The component shall not mark packets as authenticated where the computed MAC does not match the security trailer MAC. | Unit Test | +| AUTH006 | For any parseable frame, the component shall remove the sequence number and Security Trailer and forward the remaining packet data with the verification result recorded in the frame context. | Inspection, Integration Test | +| AUTH007 | The component shall provide a command and telemetry channel to report the current sequence number to enable ground station synchronization. | Inspection, Integration Test | + +Opcode-based bypass policy (formerly AUTH002) is owned by ProvesRouter; see its SDD. + +## Change Log + +| Date | Description | +| --- | --- | +| 2025-11-26 | Initial design. | +| 2026-07-17 | Renamed to TcSecurityDeframer, refactor to discrete responsibilities: Authenticator, Parser, Validator. Pass-through interface between TcDeframer and SpacePacketDeframer; verification result carried in frame context; policy enforcement moved to ProvesRouter. | +| 2026-08-29 | Renamed to TcSecurityDecryptor and moved onto the upstream F Prime 4.3.0 SDLS hook: implements `Svc.Ccsds.CcsdsSdlsDecrypt` behind `Svc::Ccsds::CcsdsSdlsDeframer` instead of owning deframing/SA-extraction directly. The SA index is now a port argument rather than a parsed field; the MAC covers the SA index via the PSA multipart API. No `SdlsSaRouter` or `SdlsFileKeyManager` adopted (see "Relationship to CcsdsSdlsDeframer" above). | diff --git a/docs-site/components/TcSecurityDeframer.md b/docs-site/components/TcSecurityDeframer.md deleted file mode 100644 index cbdc9687..00000000 --- a/docs-site/components/TcSecurityDeframer.md +++ /dev/null @@ -1,209 +0,0 @@ -# Components::TcSecurityDeframer - -The TcSecurityDeframer component implements the TC ProcessSecurity flow of CCSDS 355.0-B-2 in the uplink path. It sits between TcDeframer and SpacePacketDeframer: it parses the Security Header and Trailer, validates the SPI and anti-replay sequence number, verifies the HMAC, then strips the security envelope and forwards the frame with the verification result recorded in the frame context (`authenticated` flag). - -The component does not enforce policy. Frames that fail verification are still forwarded (unauthenticated) so that downstream policy โ€” owned by ProvesRouter and its opcode bypass allowlist โ€” can decide whether to route or reject them. This keeps knowledge of packet structure here and knowledge of policy at the edge. - -## Overview - -The component is a thin stateful shell over pure-function namespaces: - -- `Ccsds355_0_B_2::parse` (Parser) โ€” Security Header (SPI, sequence number) and Trailer (MAC) extraction -- `Components::validatePacket` (Validator) โ€” SPI validation and anti-replay sequence-number window validation -- `Components::authenticatePacket` / `importHmacKey` (Authenticator) โ€” HMAC-SHA-256 (truncated to 16 bytes) verification via PSA crypto - -The only component state is the last accepted sequence number (mutex-guarded, persisted to file) and the imported HMAC key id. - -Primary data path connections: - -- TcDeframer.dataOut -> TcSecurityDeframer.dataIn -- TcSecurityDeframer.dataOut -> SpacePacketDeframer.dataIn -- TcSecurityDeframer.dataReturnOut -> TcDeframer.dataReturnIn -- SpacePacketDeframer.dataReturnOut -> TcSecurityDeframer.dataReturnIn - -## Class Diagram - -```mermaid -classDiagram -direction LR - -class TcSecurityDeframer { - +configure() - -dataIn_handler(portNum, data, context) - -dataReturnIn_handler(portNum, data, context) - -GET_SEQ_NUM_cmdHandler(opCode, cmdSeq) - -SET_SEQ_NUM_cmdHandler(opCode, cmdSeq, seqNum) - -readSequenceNumber(value) - -writeSequenceNumber(value) - -m_sequenceNumber : U32 - -m_sequenceNumberWindow : U32 - -m_hmacKeyId : uint32_t -} - -class Ccsds355_0_B_2 { - <> - +parse(buffer, size) Parser_Result -} - -class PacketValidator { - <> - +validatePacket(secHeader, sequenceNumber, window) Status -} - -class PacketAuthenticator { - <> - +importHmacKey(key, keyId) KeyImportResult - +authenticatePacket(buffer, size, mac, keyId) AuthenticationResult -} - -class TCSecurityHeader { - +spi : uint32_t - +sequenceNumber : uint32_t -} - -class TCSecurityTrailer { - +mac : Mac -} - -class Mac { - <> - +std::array~uint8_t,16~ -} - -TcSecurityDeframer ..> Ccsds355_0_B_2 : parses -TcSecurityDeframer ..> PacketValidator : validates -TcSecurityDeframer ..> PacketAuthenticator : authenticates -Ccsds355_0_B_2 --> TCSecurityHeader : returns -Ccsds355_0_B_2 --> TCSecurityTrailer : returns -TCSecurityTrailer --> Mac : contains -``` - -## Packet Format - -TcDeframer strips the TC Primary Header and FECF before this component, so the buffer received on dataIn is: - -- Security Header (6 bytes): SPI (2) + Sequence Number (4) -- Space Packet Primary Header (6 bytes) -- Space Packet Data Field (includes F Prime command) -- Security Trailer (16-byte MAC) - -Output packet layout (forwarded per CCSDS 355.0-B-2 ยง3.3.3.3): - -- Space Packet Primary Header (6 bytes) -- Space Packet Data Field - -The MAC is HMAC-SHA-256 truncated to 16 bytes, computed over the Security Header and Data Field (everything except the Security Trailer). - -### Additional resources - -- [CCSDS 355.0-B-2 Space Data Link Security Protocol](https://ccsds.org/Pubs/355x0b2.pdf) - -## Behavior - -1. Parse the Security Header and Trailer. If the frame is too short to contain them it cannot be stripped for downstream deframing: log ParsingFailed and return the buffer upstream (drop). -2. Validate the SPI (only SPI 0 is currently supported) and the anti-replay sequence number (must be strictly ahead of the last accepted value, within SEQ_NUM_WINDOW, with U32 wraparound handled). -3. If validation passes, verify the MAC. -4. Only when all checks pass: store and persist the received sequence number, telemeter it, and set `authenticated = true` in the frame context. Frames failing any check never advance the sequence number (issue #426). -5. Strip the Security Header and Trailer and forward on dataOut with the resulting `authenticated` flag. ProvesRouter rejects unauthenticated packets unless their opcode is on the bypass allowlist. - -At startup, `configure()` loads the persisted sequence number and telemeters it so the first downlinked value is correct before any command is accepted (issue #427). - -## Parameters - -| Name | Type | Default | Description | -|---|---|---|---| -| SEQ_NUM_WINDOW | U32 | 50000 | Maximum allowed forward sequence-number distance before rejecting a packet as out-of-window. | -| SEQ_NUM_FILE_PATH | string | "//sequence_number.txt" | File path used to persist and restore the sequence number across restarts. | - -## Port Descriptions - -| Name | Direction | Type | Description | -|---|---|---|---| -| dataIn | Input (guarded) | Svc.ComDataWithContext | Receives frames from TcDeframer for parse, validation, and authentication. | -| dataReturnIn | Input (sync) | Svc.ComDataWithContext | Receives returned ownership for buffers previously sent through dataOut. | -| dataOut | Output | Svc.ComDataWithContext | Forwards the stripped frame downstream with the authenticated flag set in the context. | -| dataReturnOut | Output | Svc.ComDataWithContext | Returns ownership of structurally invalid frames (and relays dataReturnIn ownership upstream). | - -Standard AC ports are also present for command handling, events, telemetry, parameter access, and time. - -## Telemetry Channels - -| Name | Type | Description | -|---|---|---| -| CurrentSequenceNumber | U32 | Current accepted sequence number tracked by the component. Emitted at startup and on each accepted packet. | - -Routed/bypassed/rejected packet counts are telemetered by ProvesRouter, which owns the accept/reject policy. - -## Events - -| Name | Severity | Parameters | Description | -|---|---|---|---| -| SequenceNumberGet | Activity High | seq_num: U32 | Logged by GET_SEQ_NUM on successful read. Format: "Sequence number is {}" | -| SequenceNumberReadFailed | Warning High (throttle 2) | status: Os.FileStatus | Logged when sequence-number read fails. Format: "Failed to read sequence number, error: {}" | -| SequenceNumberSet | Activity High | seq_num: U32 | Logged by SET_SEQ_NUM on successful write. Format: "Sequence number set to {}" | -| SequenceNumberWriteFailed | Warning High (throttle 2) | status: Os.FileStatus | Logged when sequence-number write fails. Format: "Failed to write sequence number, error: {}" | -| SequenceNumberInvalid | Warning High (throttle 2) | packet_seq_num: U32, seq_num: U32, window: U32 | Logged when anti-replay validation fails. Format: "Sequence number less than last accepted or out of window: Received={}, LastAccepted={}, Window={}" | -| AuthenticationFailed | Warning High (throttle 2) | auth_status: PacketAuthenticatorStatus, rc: I32 | Logged when MAC verification fails. Format: "Authentication failed: Status={}, PSA Return Code={}" | -| ParsingFailed | Warning High (throttle 2) | parse_status: PacketParserStatus | Logged when frame parsing fails. Format: "Parsing failed: {}" | -| SpiInvalid | Warning High (throttle 2) | packet_spi: U32 | Logged when SPI validation fails. Format: "SPI invalid: Received={}" | - -## Commands - -| Name | Type | Parameters | Description | -|---|---|---|---| -| GET_SEQ_NUM | Sync | None | Reads and reports the current sequence number (SequenceNumberGet event). | -| SET_SEQ_NUM | Sync | seq_num: U32 | Sets and persists a new sequence number (SequenceNumberSet event). | - -## Unit Tests - -TcSecurityDeframer helper functionality is covered by unit tests in PROVESFlightControllerReference/test/unit-tests: - -| Test File | Coverage | -|---|---| -| test_TcSecurityDeframer_Parser.cpp | Valid parse path plus parse failures for SPI, sequence number, and MAC size checks. | -| test_TcSecurityDeframer_Validator.cpp | SPI validation, out-of-window and replayed sequence numbers, window boundary, and wraparound handling. | -| test_TcSecurityDeframer_Authenticator.cpp | Key import failures, successful MAC verification, and failed verification with corrupted MAC or data. | - -Run unit tests with: - -```bash -make test-unit -``` - -## GDS Plugin - -To send authenticated packets from GDS, build the framing plugin: - -```bash -make framer-plugin -``` - -Then run GDS with the framing plugin enabled as configured by the project tooling. - -## Generating Keys - -The default authentication key header (AuthDefaultKey.h) is generated at build time from project key material via `make generate-auth-key` or `make copy-secrets`. This generated file is machine-local and not committed. - -## Requirements - -| Name | Description | Validation | -|---|---|---| -| AUTH001 | The component shall parse incoming frames to extract the SPI, sequence number, and MAC fields. | Unit Test | -| AUTH003 | The component shall validate that the SPI value corresponds to a configured Security Association. | Unit Test | -| AUTH004 | The component shall validate the received sequence number against the stored sequence number. | Unit Test | -| AUTH004-A | The component shall not authenticate packets with sequence numbers that are outside the acceptable window and shall log an event. | Unit Test, Inspection | -| AUTH004-B | The component shall set the stored sequence number to the sequence number transmitted in the packet only when a packet is fully validated and authenticated. | Inspection | -| AUTH004-C | The component shall allow the sequence number window to be configurable via a parameter. | Inspection | -| AUTH005 | The component shall compute the MAC over the entire frame minus the last 16-byte security trailer. | Unit Test | -| AUTH005-A | The component shall not mark packets as authenticated where the computed MAC does not match the security trailer MAC. | Unit Test | -| AUTH006 | For any parseable frame, the component shall remove the Security Header and Security Trailer and forward the remaining packet data with the verification result recorded in the frame context. | Inspection, Integration Test | -| AUTH007 | The component shall provide a command and telemetry channel to report the current sequence number to enable ground station synchronization. | Inspection, Integration Test | - -Opcode-based bypass policy (formerly AUTH002) is owned by ProvesRouter; see its SDD. - -## Change Log - -| Date | Description | -| --- | --- | -| 2025-11-26 | Initial design. | -| 2026-07-17 | Renamed to TcSecurityDeframer, refactor to discrete responsibilities: Authenticator, Parser, Validator. Pass-through interface between TcDeframer and SpacePacketDeframer; verification result carried in frame context; policy enforcement moved to ProvesRouter. | diff --git a/docs-site/index.md b/docs-site/index.md index 9bee2288..15b70280 100644 --- a/docs-site/index.md +++ b/docs-site/index.md @@ -38,8 +38,8 @@ Low-level driver components for specific hardware peripherals: [Drv2605 Manager] ### [Storage Components](components/FlashWorker.md) Components that manage persistent storage and filesystem operations: [Flash Worker](components/FlashWorker.md), [Fs Format](components/FsFormat.md), [Fs Space](components/FsSpace.md), [Null Prm Db](components/NullPrmDb.md) -### [Security Components](components/TcSecurityDeframer.md) -Components that handle authentication and security features: [TcSecurityDeframer](components/TcSecurityDeframer.md), [PROVES Router](components/ProvesRouter.md) +### [Security Components](components/TcSecurityDecryptor.md) +Components that handle authentication and security features: [TcSecurityDecryptor](components/TcSecurityDecryptor.md), [PROVES Router](components/ProvesRouter.md) ### [Additional Resources](additional-resources/board-list.md) Troubleshooting guides and additional configuration information: diff --git a/mkdocs.yml b/mkdocs.yml index b2e7fb3d..cfa20017 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -101,7 +101,7 @@ nav: - Fs Space: components/FsSpace.md - Null Prm Db: components/NullPrmDb.md - Security Components: - - Tc Security Deframer: components/TcSecurityDeframer.md + - Tc Security Decryptor: components/TcSecurityDecryptor.md - PROVES Router: components/ProvesRouter.md - Additional Resources: - Troubleshooting: additional-resources/troubleshooting.md diff --git a/scripts/generate_auth_key_header.py b/scripts/generate_auth_key_header.py index 17636423..31fc5f14 100755 --- a/scripts/generate_auth_key_header.py +++ b/scripts/generate_auth_key_header.py @@ -73,7 +73,7 @@ def main(): parser.add_argument( "--output", type=str, - default="PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h", + default="PROVESFlightControllerReference/Components/TcSecurityDecryptor/AuthDefaultKey.h", help="Output path for AuthDefaultKey.h", ) parser.add_argument(