Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
abe8f9c
Initial plan
Copilot Oct 6, 2025
cd2a044
Implement HMAC command authentication with replay attack prevention
Copilot Oct 6, 2025
9d074fd
Fix typecheck errors: convert counter to int and use string for HMAC …
Copilot Oct 6, 2025
51b13f8
Merge branch 'main' into copilot/fix-7bfb9c49-466e-42db-a6be-dece956d…
Mikefly123 Oct 6, 2025
b4b7be8
Remove password fallback, add NVM counter storage, implement 16-bit c…
Copilot Oct 7, 2025
3095769
Add comprehensive HMAC integration tests between ground station and f…
Copilot Oct 7, 2025
3aae5f2
Appease Linter
Mikefly123 Oct 7, 2025
32e3f17
Add counter prompt, external HMAC library, and large message integrat…
Copilot Oct 7, 2025
d0a665e
Using Circuitpython Hmac library
ineskhou Oct 7, 2025
6dedc2f
Fix HMAC library imports to work with both CircuitPython and CPython …
Copilot Oct 7, 2025
26db833
Update hmac_auth.py
ineskhou Oct 8, 2025
91d72ed
Last COmmand Counter Default 1
ineskhou Oct 8, 2025
0497842
Update hmac_auth.py
ineskhou Oct 8, 2025
6f5638b
Added compare_digest bc circuitpython and also bc timerattack
ineskhou Oct 8, 2025
7b22d8c
Fix debugger
nateinaction Oct 9, 2025
9547082
show mock and lint
nateinaction Oct 9, 2025
183f6b1
Fix large_message test
nateinaction Oct 9, 2025
a69219d
Merge branch 'copilot/fix-7bfb9c49-466e-42db-a6be-dece956d6d8c' of gi…
ineskhou Oct 9, 2025
99694a9
coorected more tests (thx nate)
ineskhou Oct 9, 2025
76e657d
finished correcting tests for hmac library
ineskhou Oct 10, 2025
afd00a0
added functions for send counter (cdh) and compare sigest
ineskhou Oct 10, 2025
c2ccbac
fixed typecheck
ineskhou Oct 10, 2025
ba658ed
fixed typechek and tests bc of typecheck AHHH
ineskhou Oct 10, 2025
c15c753
remove prints
ineskhou Oct 10, 2025
828acc3
added ground station command counter
ineskhou Oct 10, 2025
7904625
fixed typecheck
ineskhou Oct 10, 2025
f0f398c
put launch back to b4
ineskhou Oct 10, 2025
6f823df
added joke configuation python
ineskhou Oct 10, 2025
7440fa1
updated counter
ineskhou Oct 10, 2025
a9fe3d1
correct format
ineskhou Oct 10, 2025
49d44c9
aknoagement
ineskhou Oct 10, 2025
46bf643
process counter better
ineskhou Oct 10, 2025
67fb3d2
finally udpated the tests
ineskhou Oct 11, 2025
9090797
fixed a test bc converting to string, added more debug messages
ineskhou Oct 14, 2025
ee24f3c
update loge
ineskhou Oct 14, 2025
56d8872
debugging listening
ineskhou Oct 14, 2025
9af4e74
appease linter
ineskhou Oct 14, 2025
3625263
truing to figure ouw where in authen it silcently failts
ineskhou Oct 14, 2025
5f16d89
truing to figur vighfg
ineskhou Oct 14, 2025
28b4b35
removing bytes just string for comparing
ineskhou Oct 14, 2025
d94f87b
seeing how the generation hapens
ineskhou Oct 14, 2025
d0991c5
linkt
ineskhou Oct 15, 2025
48a6d46
help debug
ineskhou Oct 18, 2025
1ad1770
decoding not
ineskhou Oct 29, 2025
5d70b55
new print without string
ineskhou Oct 29, 2025
7eed3e8
updated packet manager
ineskhou Oct 29, 2025
fc841bd
remove the eating of the responses
ineskhou Oct 29, 2025
d32bb27
fixing value rrror
ineskhou Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 71 additions & 14 deletions circuitpython-workspaces/flight-software/src/pysquared/cdh.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from .config.config import Config
from .hardware.radio.packetizer.packet_manager import PacketManager
from .hmac_auth import HMACAuthenticator
from .logger import Logger


Expand Down Expand Up @@ -55,6 +56,12 @@ def __init__(
self._config: Config = config
self._packet_manager: PacketManager = packet_manager
self._send_delay: float = send_delay
self._hmac_authenticator: HMACAuthenticator = HMACAuthenticator(
config.hmac_secret
)
self._last_valid_counter: int = (
-1
) # Track last valid counter for replay prevention

def listen_for_commands(self, timeout: int) -> None:
"""Listens for commands from the radio and handles them.
Expand All @@ -73,7 +80,7 @@ def listen_for_commands(self, timeout: int) -> None:

msg: dict[str, str] = json.loads(json_str)

# Check for OSCAR password first
# Check for OSCAR password first (legacy authentication)
if msg.get("password") == self.oscar_password:
self._log.debug("OSCAR command received", msg=msg)
cmd = msg.get("command")
Expand All @@ -96,20 +103,70 @@ def listen_for_commands(self, timeout: int) -> None:
self.oscar_command(cmd, args)
return

# If message has password field, check it
if msg.get("password") != self._config.super_secret_code:
self._log.debug(
"Invalid password in message",
msg=msg,
)
return
# New HMAC-based authentication
hmac_value = msg.get("hmac")
counter_raw = msg.get("counter")

if msg.get("name") != self._config.cubesat_name:
self._log.debug(
"Satellite name mismatch in message",
msg=msg,
)
return
if hmac_value is None or counter_raw is None:
# Fall back to password-based authentication for backward compatibility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should still have the password based authentication because this will be significantly more vulnerable. Let's remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed password-based authentication fallback in commit a6c77f3. Now only HMAC authentication is accepted for non-OSCAR commands, eliminating the security vulnerability.

if msg.get("password") != self._config.super_secret_code:
self._log.debug(
"Invalid password in message",
msg=msg,
)
return

if msg.get("name") != self._config.cubesat_name:
self._log.debug(
"Satellite name mismatch in message",
msg=msg,
)
return
else:
# Use HMAC authentication
# Convert counter to int
try:
counter: int = int(counter_raw)
except (ValueError, TypeError):
self._log.debug(
"Invalid counter in message",
counter=counter_raw,
)
return

# Extract message without HMAC for verification
msg_without_hmac = {k: v for k, v in msg.items() if k != "hmac"}
message_str = json.dumps(msg_without_hmac, separators=(",", ":"))

# Verify HMAC
if not self._hmac_authenticator.verify_hmac(
message_str, counter, hmac_value
):
self._log.debug(
"Invalid HMAC in message",
msg=msg,
)
return

# Prevent replay attacks - counter must be greater than last valid counter
if counter <= self._last_valid_counter:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are two possible flaws with this system:

  1. Right now we are only storing the counter in runtime RAM, as a result if we reset the local counter will fall back to Zero and replay attacks are now possible again. Let's address this by storing the counter in NVM instead.
  2. Once the counter is in NVM, it will need to have a fixed size, like 8-bits or 16-bits. We want to make sure that we can still capture valid commands, even if there are many missed packets since the last command

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed both issues in commit a6c77f3:

  1. NVM Storage: Created Counter16 class that stores the last valid counter in NVM using 2 bytes. The counter persists across resets, preventing replay attacks after power cycles.

  2. Wraparound Handling: Implemented smart 16-bit counter with wraparound detection. Valid commands can have counters 1-32768 positions ahead (allowing for many missed packets). Backwards or replayed counters are rejected. Example: counter 10 is valid after 65530 (wraparound), but counter 40 after 50 is rejected (backwards).

All 426 tests passing including 9 new Counter16 tests and 2 wraparound tests.

self._log.debug(
"Replay attack detected - counter not greater than last valid",
counter=counter,
last_valid=self._last_valid_counter,
)
return

# Update last valid counter
self._last_valid_counter = counter

# Verify satellite name
if msg.get("name") != self._config.cubesat_name:
self._log.debug(
"Satellite name mismatch in message",
msg=msg,
)
return

# If message has command field, execute the command
cmd = msg.get("command")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ class Config:
critical_battery_voltage (float): Critical battery voltage.
reboot_time (int): Time before reboot in seconds.
turbo_clock (bool): Turbo clock enabled flag.
super_secret_code (str): Secret code for special operations.
super_secret_code (str): Secret code for special operations (deprecated).
repeat_code (str): Code for repeated operations.
hmac_secret (str): Shared secret for HMAC command authentication.
longest_allowable_sleep_time (int): Maximum allowable sleep time.
CONFIG_SCHEMA (dict): Validation schema for configuration keys.

Expand Down Expand Up @@ -114,6 +115,7 @@ def __init__(self, config_path: str) -> None:
self.turbo_clock: bool = json_data["turbo_clock"]
self.super_secret_code: str = json_data["super_secret_code"]
self.repeat_code: str = json_data["repeat_code"]
self.hmac_secret: str = json_data.get("hmac_secret", "default_hmac_secret")
self.longest_allowable_sleep_time: int = json_data[
"longest_allowable_sleep_time"
]
Expand All @@ -122,6 +124,7 @@ def __init__(self, config_path: str) -> None:
"cubesat_name": {"type": str, "min_length": 1, "max_length": 10},
"super_secret_code": {"type": bytes, "min": 1, "max": 24},
"repeat_code": {"type": bytes, "min": 1, "max": 4},
"hmac_secret": {"type": bytes, "min": 16, "max": 64},
"normal_charge_current": {"type": float, "min": 0.0, "max": 2000.0},
"normal_battery_voltage": {"type": float, "min": 6.0, "max": 8.4},
"degraded_battery_voltage": {"type": float, "min": 5.4, "max": 8.0},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""This module provides HMAC-based authentication for command messages.
This module implements HMAC (Hash-based Message Authentication Code) for
authenticating commands sent to the satellite. It provides protection against
unauthorized commands and replay attacks through the use of a shared secret
and packet counter.
**Usage:**
```python
from pysquared.hmac_auth import HMACAuthenticator
# On the ground station
authenticator = HMACAuthenticator("shared_secret_key")
message = '{"command": "send_joke", "name": "MySat"}'
counter = 42
hmac_value = authenticator.generate_hmac(message, counter)
# On the satellite
authenticator = HMACAuthenticator("shared_secret_key")
is_valid = authenticator.verify_hmac(message, counter, hmac_value)
```
"""

import hmac
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think hmac is natively available in CircuitPython. Can you make sure to update the pyproject.toml to pull in this open source library that provides it:

https://github.com/jimbobbennett/CircuitPython_HMAC.git

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in commit 8f6e2a1 to use external CircuitPython HMAC library from https://github.com/jimbobbennett/CircuitPython_HMAC.git. Added dependency to pyproject.toml and updated imports with fallback for testing compatibility.



class HMACAuthenticator:
"""Provides HMAC authentication for command messages."""

def __init__(self, secret_key: str) -> None:
"""Initializes the HMACAuthenticator.
Args:
secret_key: The shared secret key for HMAC generation and verification.
"""
self._secret_key: bytes = secret_key.encode("utf-8")

def generate_hmac(self, message: str, counter: int) -> str:
"""Generates an HMAC for a message with a counter.
Args:
message: The message to authenticate.
counter: The packet counter for replay attack prevention.
Returns:
The HMAC as a hexadecimal string.
"""
# Combine message and counter
data = f"{message}|{counter}".encode("utf-8")

# Generate HMAC using SHA-256
# Use string "sha256" for CircuitPython compatibility
h = hmac.new(self._secret_key, data, "sha256")
return h.hexdigest()

def verify_hmac(self, message: str, counter: int, received_hmac: str) -> bool:
"""Verifies an HMAC for a message with a counter.
Args:
message: The message to verify.
counter: The packet counter for replay attack prevention.
received_hmac: The HMAC to verify.
Returns:
True if the HMAC is valid, False otherwise.
"""
expected_hmac = self.generate_hmac(message, counter)
return hmac.compare_digest(expected_hmac, received_hmac)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pysquared.cdh import CommandDataHandler
from pysquared.config.config import Config
from pysquared.hardware.radio.packetizer.packet_manager import PacketManager
from pysquared.hmac_auth import HMACAuthenticator
from pysquared.logger import Logger


Expand All @@ -27,6 +28,8 @@ def __init__(
self._config = config
self._packet_manager = packet_manager
self._cdh = cdh
self._hmac_authenticator = HMACAuthenticator(config.hmac_secret)
self._command_counter = 0 # Counter for replay attack prevention
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the ground station code might restart many times before the flight software counter is rolled over, let's make it so that the user is prompted to enter a counter to start on when the code starts running.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added counter prompt in commit 8f6e2a1. Ground station now prompts user to enter starting counter value (0-65535) on startup, allowing operators to resume from where they left off if the ground station restarts.


def listen(self):
"""Listen for incoming packets from the satellite."""
Expand Down Expand Up @@ -86,7 +89,6 @@ def handle_input(self, cmd_selection):

message: dict[str, object] = {
"name": self._config.cubesat_name,
"password": self._config.super_secret_code,
}

if cmd_selection == "1":
Expand All @@ -98,6 +100,17 @@ def handle_input(self, cmd_selection):
elif cmd_selection == "3":
message["command"] = self._cdh.command_send_joke

# Increment counter for replay attack prevention
self._command_counter += 1
message["counter"] = self._command_counter

# Generate HMAC for the message
message_str = json.dumps(message, separators=(",", ":"))
hmac_value = self._hmac_authenticator.generate_hmac(
message_str, self._command_counter
)
message["hmac"] = hmac_value

while True:
# Turn on the radio so that it captures any received packets to buffer
self._packet_manager.listen(1)
Expand All @@ -107,6 +120,7 @@ def handle_input(self, cmd_selection):
"Sending command",
cmd=message["command"],
args=message.get("args", []),
counter=self._command_counter,
)
self._packet_manager.send(json.dumps(message).encode("utf-8"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@
"repeat_code": "RP",
"sleep_duration": 30,
"super_secret_code": "ABCD",
"hmac_secret": "test_hmac_secret_key",
"turbo_clock": false
}
Loading
Loading