Astro~/0.4
** Docs
** Astro * Docs
** /docs/reference/security

Security

A library whose input arrives from a radio. The threat model, the resource limits, and how to report a problem.

Astro parses octets that came off a radio link. Nothing upstream of the decoder is trusted, or can be: a ground station receives whatever is transmitted on its frequency, and a spacecraft receives whatever is transmitted at it.

So the decoders are the attack surface, and this page is what is done about that.

The threat model

In scope. Malformed, truncated, or hostile octets reaching any Decode function. The failure modes that matter are a panic that takes down a ground system, and a length field that makes the decoder allocate memory the attacker chose.

Out of scope for this library. Whether the sender is who they claim to be. That is SDLS's job, and it is a separate decision a mission makes: authenticate the uplink so a command cannot be forged, encrypt the downlink if the data is sensitive. pkg/sdls implements the mechanism; it cannot make you turn it on.

Also out of scope. Key management. pkg/sdls takes the key octets and does not load, store, rotate or protect them.

Every decoder is fuzzed

84 fuzz targets across 32 packages. The property each one asserts is that arbitrary input never panics and never allocates from a length field the input controls.

make fuzz-smoke

runs a short burst — 15 seconds per target by default — over 74 of them, each with its own go test -run '^$' -fuzz line in the Makefile. The remaining 10 are extra targets in pkg/pus, pkg/pxsc and pkg/xtce that are not yet wired into that list, and still have to be run by hand with go test -fuzz. A new decoder is expected to arrive with a target already in fuzz-smoke, per the contributing guide.

Resource limits

A length field in a header is a number an attacker picks. Where a standard sets no ceiling, this library sets one, because the alternative is make([]byte, attackerChoice).

PackageLimitValueWhy
pkg/bpMaxReassembledADU64 MiB
pkg/ltpDefaultMaxBlockSize64 MiB
pkg/ltpDefaultMaxOutstandingReports1024
pkg/cfdpDefaultMaxFileSize64 MiB
pkg/cfdpDefaultMaxSegments65536
pkg/cfdpMaxIDWidth8
pkg/cfdpMaxSegmentMetadataSize63
pkg/pxdlDefaultMaxPacketSize64 KiB
pkg/pxdlDefaultMaxPending32
pkg/ldcWithMaxSamples2^28 samples
pkg/xtceMaxDocumentSize64 MiB
pkg/xtceMaxDepth100
pkg/xtceMaxFields2^18
pkg/xtceMaxRepeatCount2^16
pkg/sleDefaultMaxMessageSize16 MiB
pkg/sleDefaultMaxLength16 MiB
pkg/sleMaxSpaceLinkDataUnit65536
pkg/sleMaxEventQualifier1024
pkg/sdnvMaxEncodedSize10

Where the limit is a Default, it is a configurable field: set it to what your mission actually sends rather than leaving headroom you do not need.

XML, and why the usual attacks do not apply

pkg/xtce reads XML, which normally means worrying about external entities. Go's encoding/xml does not fetch DTDs and does not expand external entities, so XXE, entity expansion bombs and network callbacks from a document are not reachable. What remains is plain resource abuse, and that is what MaxDocumentSize and MaxDepth bound.

pkg/ndm reads XML too, through its own reader rather than a struct unmarshal, and caps element nesting at 64 levels. That cap is not configurable: a conforming navigation message nests about six levels, and without a ceiling a few tens of megabytes of repeated open tags would drive the goroutine stack past its limit, which no recover catches.

There is no XSD validation. The standard library has no XSD validator and this library takes no dependencies, so Validate runs semantic checks written in Go: references resolve, inheritance does not loop, names do not collide. A file that breaks the schema in a way those checks miss will load. Run xmllint over it first if that matters.

Cryptography

pkg/sdls uses the Go standard library for AES-GCM and AES-CMAC. No cryptographic primitive is implemented here.

Two details worth knowing:

ISP1 credentials hash with SHA-256, per CCSDS 913.1-B-2 clause 3.1.2.3. SHA-1 belonged to the previous issue of the standard. A 20-octet legacy digest still decodes, but it cannot be verified, because the superseded scheme is not implemented. Only SHA-256 is ever generated.

A Security Association is not safe for concurrent use. It holds the sender's IV counter and the receiver's anti-replay state, both of which change on every frame. Sharing one across goroutines corrupts the IV counter, and reusing an IV under GCM is the failure that loses you the key stream. One SA per direction per channel.

What a mission still has to do

  • Turn on authentication for commanding. An unauthenticated uplink is forgeable by anyone with a transmitter and the published frame format. See encrypt and authenticate a link.
  • Set SeqWindow. pkg/sdls disables the anti-replay check when it is zero, which is only ever right in a test.
  • Bind each SA to its channels. Use ProcessSecurityForChannel, not ProcessSecurity, so a genuine frame moved to another virtual channel is refused.
  • Keep clocks agreeing. SLE credentials are rejected when the peer's timestamp is too far from now, which is the replay defence and also a way to lock yourself out.
  • Decide what a decode failure means. Accept treats an undecodable CADU as an error because only the caller can tell a corrupt frame from a misconfigured channel. A station should log and continue rather than stop.

Reporting a vulnerability

Use private vulnerability reporting on the repository. Please do not open a public issue, discussion or pull request for it.

Include what you investigated and why you believe an exploit is possible. A proof of concept and a link to the code are the most useful things you can send. Reports are not eligible for a bounty.

Reference