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-smokeruns 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).
| Package | Limit | Value | Why |
|---|---|---|---|
pkg/bp | Max | 64 MiB | |
Total arrives on the wire, so without a ceiling one small fragment claiming a huge total makes Reassemble allocate it. RFC 9171 sets no limit. Decoding a whole bundle needs no limit: it reads from a caller-supplied slice, so a length field can only claim what is already in memory | |||
pkg/ltp | Default | 64 MiB | |
Not in RFC 5326: a segment offset is an SDNV reaching 2^64, so one corrupt segment could claim a huge offset | |||
pkg/ltp | Default | 1024 | |
A sender that never acknowledges a report would otherwise let the receiver's per-checkpoint state grow without end | |||
pkg/cfdp | Default | 64 MiB | |
A peer declares the file size, and the receiver allocates for it | |||
pkg/cfdp | Default | 65536 | |
Bounds the received-data set a lossy transfer builds, well above what an ordinary one fragments into | |||
pkg/cfdp | MaxIDWidth | 8 | |
Entity ID widths are caller-declared | |||
pkg/cfdp | Max | 63 | |
The widest a 6-bit length field can describe, per clause 5.3 table 5-14 | |||
pkg/pxdl | Default | 64 KiB | |
The standard sets no ceiling on a reassembled packet, so a stream that never ends one grows it forever | |||
pkg/pxdl | Default | 32 | |
How many routing IDs may hold a partial packet at once | |||
pkg/ldc | With | 2^28 samples | |
A zero-block run costs a handful of input bits and yields thousands of samples, so an unbounded Decompress can grow output for as long as the input lasts | |||
pkg/xtce | Max | 64 MiB | |
A very large database file | |||
pkg/xtce | MaxDepth | 100 | |
A deeply nested document. The check runs as a token scan before any decoding, so deep input is refused rather than recursed into | |||
pkg/xtce | MaxFields | 2^18 | |
How many fields one Layout may hold, once repeats and inheritance have been spliced together | |||
pkg/xtce | Max | 2^16 | |
On the dynamic path a repeat count comes straight out of the packet being parsed | |||
pkg/sle | Default | 16 MiB | |
A TML header promising more than exists | |||
pkg/sle | Default | 16 MiB | |
What one BER decoder accepts from a length field | |||
pkg/sle | Max | 65536 | |
Ceiling on a delivered frame | |||
pkg/sle | Max | 1024 | |
Bounded opaque field | |||
pkg/sdnv | Max | 10 | |
A 64-bit value needs at most 10 octets; more is a malformed or padded encoding | |||
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/sdlsdisables the anti-replay check when it is zero, which is only ever right in a test. - Bind each SA to its channels. Use
ProcessSecurityForChannel, notProcessSecurity, 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.
Accepttreats 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
- How this is verified, the fuzzing and vector picture in full
- Encrypt and authenticate a link, SDLS in practice
- SDLS protocol page | XTCE protocol page