sdls

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package sdls implements the Space Data Link Security Protocol per CCSDS 355.0-B-2 (July 2022).

SDLS protects the data field of a TM, TC, AOS, or USLP Transfer Frame. It inserts a Security Header before the frame data and a Security Trailer carrying a Message Authentication Code after it. The carrier frame packages need no changes: the caller builds the protected data field with this package and hands the result to the frame constructor.

                  ┌──────────── carrier frame data field ────────────┐
frame header ...  │ Security Header │ data (or ciphertext) │ Trailer │
                  └──────────────────────────────────────────────────┘

The wire layout of the Security Header is not self-describing. Field widths come from the Security Association named by the Security Parameter Index, which both ends agree on before the link opens (clause 2.3.1.4).

This package ships the annex baselines: the AES-256-GCM authenticated encryption of clause E1 (TM), clause E3 (AOS) and clause E4 (USLP), and the AES-CMAC authentication of clause E2 (telecommand). It also offers GMAC, not an annex baseline itself, but the authentication-only companion of the GCM baselines. Pick between the two MAC algorithms with SecurityAssociation.AuthAlgorithm.

Index

Constants

View Source
const (
	// SPIReservedZero is reserved by CCSDS for future use.
	SPIReservedZero uint16 = 0
	// SPIReservedAllOnes is reserved by CCSDS for future use.
	SPIReservedAllOnes uint16 = 65535
)

Reserved Security Parameter Index values, per clause 4.1.1.2.3.

View Source
const (
	MinMACSize  = 12
	MinCMACSize = 1
	MaxMACSize  = 16
)

MAC widths this package accepts, in octets. The clause E1 and clause E2 baselines both use 16. Clause 4.2.3.4 f) allows truncating a MAC by dropping its least significant bits, which is the same most-significant-bits-first truncation that crypto/cipher.NewGCMWithTagSize performs.

MinMACSize is the floor for GCM and GMAC only: Go's crypto/cipher refuses GCM tags below 12 octets. CMAC has no such constraint, SP 800-38B clause 6.4 permits any truncation, so a CMAC SA may declare MinCMACSize to MaxMACSize.

View Source
const AESKeySize = 32

AESKeySize is the key width this package accepts, in octets (256 bits, per clause E1.1 a).

View Source
const GCMIVSize = 12

GCMIVSize is the initialization vector width GCM requires, in octets (96 bits, per clause E1.1 b).

View Source
const MaxSecurityHeaderSize = 64

MaxSecurityHeaderSize is the largest permitted Security Header, per CCSDS 355.0-B-2 clause 4.1.1.1.4.

View Source
const NoMAP = -1

NoMAP marks a ChannelID whose frame type has no MAP multiplexing, or a USLP/TC channel where the MAP ID is not part of the binding.

View Source
const SPISize = 2

SPISize is the width of the Security Parameter Index field in bytes (16 bits, per clause 4.1.1.2.1).

Variables

View Source
var (
	// ErrDataTooShort indicates the input is shorter than the fields it must contain.
	ErrDataTooShort = errors.New("data too short for the security header or trailer")

	// ErrInvalidSPI indicates a reserved Security Parameter Index value.
	// CCSDS 355.0-B-2 clause 4.1.1.2.3 reserves all-zeros (0) and all-ones (65535).
	ErrInvalidSPI = errors.New("invalid SPI: 0 and 65535 are reserved by CCSDS")

	// ErrInvalidKey indicates the SA key is not a valid AES-256 key.
	ErrInvalidKey = errors.New("invalid key: AES-256 requires exactly 32 bytes")

	// ErrInvalidMode indicates the SA service type is zero or unknown.
	ErrInvalidMode = errors.New("invalid mode: must be Authentication, Encryption, or AuthenticatedEncryption")

	// ErrInvalidFieldLengths indicates the SA's managed field widths are unusable.
	ErrInvalidFieldLengths = errors.New("invalid field lengths for the security header or trailer")

	// ErrHeaderTooLong indicates the security header exceeds the 64-octet cap
	// of CCSDS 355.0-B-2 clause 4.1.1.1.4.
	ErrHeaderTooLong = errors.New("security header exceeds the maximum of 64 octets")

	// ErrUnsupportedMode indicates a service type this package does not implement.
	ErrUnsupportedMode = errors.New("unsupported mode: encryption without authentication is not implemented")

	// ErrUnknownSPI indicates no Security Association is registered for the SPI.
	ErrUnknownSPI = errors.New("unknown SPI: no security association registered")

	// ErrAuthenticationFailed indicates the MAC did not verify. It is returned
	// for every verification failure so that callers cannot distinguish a bad
	// tag from a malformed frame.
	ErrAuthenticationFailed = errors.New("authentication failed")

	// ErrReplayDetected indicates the anti-replay check rejected the frame,
	// per CCSDS 355.0-B-2 clause 2.3.2.3.
	ErrReplayDetected = errors.New("replay detected: sequence number rejected")

	// ErrIVExhausted indicates the initialization vector counter would wrap.
	// Reusing an IV with the same key destroys GCM's security guarantees, so
	// the SA refuses rather than wrapping.
	ErrIVExhausted = errors.New("initialization vector space exhausted for this key")

	// ErrSAChannelMismatch indicates the SA named by the SPI is not bound to
	// the channel the frame arrived on, per CCSDS 355.0-B-2 clause 4.2.4.3.
	ErrSAChannelMismatch = errors.New("security association is not bound to this channel")

	// ErrMaskTooShort indicates the authentication bit mask does not cover the
	// frame header and security header, per CCSDS 355.0-B-2 clause 4.2.2.6.2 a).
	ErrMaskTooShort = errors.New("authentication bit mask is shorter than the data it must cover")

	// ErrInvalidPadLength indicates the Pad Length field describes more fill
	// bytes than the recovered plaintext contains.
	ErrInvalidPadLength = errors.New("pad length exceeds the recovered data field")
)

Sentinel errors returned by the SDLS security protocol.

Functions

func BaselineAuthMaskAOS

func BaselineAuthMaskAOS(hasFHEC bool, insertZoneLen int, fl FieldLengths) []byte

BaselineAuthMaskAOS builds the clause 4.2.2.6.2 authentication bit mask for an AOS Transfer Frame: the 6-octet primary header, the 2-octet Frame Header Error Control when the mission uses it, and insertZoneLen octets of Insert Zone (zero when absent).

The mask covers the primary header and clears the mandatory exclusions: the FHEC (computed downstream of security), the Insert Zone (not part of the secured data), and the Initialization Vector inside the Security Header.

func BaselineAuthMaskTC

func BaselineAuthMaskTC(hasSegmentHeader bool, fl FieldLengths) []byte

BaselineAuthMaskTC builds the clause 4.2.2.6.2 authentication bit mask for a TC Transfer Frame: the 5-octet primary header, plus the 1-octet Segment Header when the virtual channel uses segmentation. Clause 4.2.2.6.2 requires the Segment Header to be covered, and TC has no mandatorily excluded header field, so the whole header is ones; only the Initialization Vector position inside the Security Header is cleared (zero octets wide under the clause E2 CMAC baseline anyway).

func BaselineAuthMaskTM

func BaselineAuthMaskTM(secondaryHeaderLen int, fl FieldLengths) []byte

BaselineAuthMaskTM builds the clause 4.2.2.6.2 authentication bit mask for a TM Transfer Frame whose header is the 6-octet primary header followed by secondaryHeaderLen octets of Frame Secondary Header (zero when absent).

The mask covers every header octet except the Master Channel Frame Count, which multiplexing rewrites after security is applied, and the Initialization Vector inside the Security Header. fl is the SA's field layout, used to size and place the security header portion of the mask.

func BaselineAuthMaskUSLP

func BaselineAuthMaskUSLP(primaryHeaderLen, insertZoneLen int, fl FieldLengths) []byte

BaselineAuthMaskUSLP builds the clause 4.2.2.6.2 authentication bit mask for a USLP Transfer Frame whose primary header (variable in USLP, 4 to 14 octets) is primaryHeaderLen octets, followed by insertZoneLen octets of Insert Zone (zero when absent).

The mask covers the whole primary header, including the MAP ID that Clause 4.2.2.6.2 requires, and clears the mandatory exclusions: the Insert Zone and the Initialization Vector inside the Security Header.

Types

type AuthAlgorithm

type AuthAlgorithm uint8

AuthAlgorithm selects the MAC algorithm used when Mode is Authentication.

CCSDS 355.0-B-2 keeps the service type and the algorithm apart, and so does this: Clause 4.2.2.4 defines the three modes, while the annexes name a different algorithm per link. Clause E1 gives GMAC for TM and clause E3/clause E4 for AOS and USLP; Clause E2 gives AES-CMAC for telecommand.

const (
	// AuthGMAC is AES-GCM over an empty plaintext with the whole
	// Authentication Payload as associated data. It is not itself an annex
	// baseline (clause E1/clause E3/clause E4 specify AES-GCM authenticated encryption) but
	// the natural authentication-only companion to them: the same cipher,
	// key and IV layout, with nothing encrypted. It is the zero value, so an
	// SA that does not choose keeps the behaviour it had before CMAC existed.
	AuthGMAC AuthAlgorithm = iota

	// AuthCMAC is AES-CMAC, the clause E2 telecommand baseline: a 256-bit key and a
	// 128-bit MAC.
	//
	// Clause E2.2 note: CMAC performs no encryption and needs no initialization
	// vector, so the clause E2 Security Header is six octets, a 16-bit SPI and a
	// 32-bit Sequence Number, with the IV and Pad Length fields zero octets
	// wide. An SA using CMAC must set FieldLengths.IV to 0.
	AuthCMAC
)

func (AuthAlgorithm) String

func (a AuthAlgorithm) String() string

String names the algorithm.

type ChannelID

type ChannelID struct {
	// TFVN is the Transfer Frame Version Number: 0 for TM, 1 for TC and AOS
	// (each per its own numbering), 12 (0b1100) for USLP.
	TFVN uint8

	// SCID is the Spacecraft Identifier.
	SCID uint16

	// VCID is the Virtual Channel Identifier.
	VCID uint8

	// MAPID extends the GVCID to a GMAP_ID for TC and USLP. Set it to NoMAP
	// when the frames have no MAP, or when the binding stops at the virtual
	// channel. The zero value is MAP 0, a real MAP: fill this field
	// explicitly.
	MAPID int
}

ChannelID names one channel an SA can be bound to: a Global Virtual Channel Identifier (Transfer Frame Version Number, Spacecraft ID, Virtual Channel ID), optionally extended to a Global MAP ID with the MAP ID of a TC or USLP frame.

Clause 4.2.2.2 requires a Security Association to apply to an agreed set of GVCIDs or GMAP_IDs, and clause 4.2.4.3 requires the receiving end to verify that the SA named by the SPI is in fact the one for the channel the frame arrived on. List the agreed channels in SecurityAssociation.Channels and call ProcessSecurityForChannel to have that check enforced.

type FieldLengths

type FieldLengths struct {
	IV     int // Initialization Vector, clause 4.1.1.3
	SeqNum int // anti-replay Sequence Number, clause 4.1.1.4
	PadLen int // Pad Length, clause 4.1.1.5
	MAC    int // Message Authentication Code in the trailer, clause 4.1.2.3
}

FieldLengths gives the octet widths of the managed Security Header and Security Trailer fields for one Security Association. Every width is fixed for the lifetime of the SA (clause 2.3.1.4, clause 4.2.2.5).

A width of zero means the field is absent: Clause 4.1.1.3.4 for the IV, Clause 4.1.1.4.4 for the Sequence Number, clause 4.1.1.5.3 for Pad Length.

func (FieldLengths) HeaderSize

func (fl FieldLengths) HeaderSize() int

HeaderSize returns the encoded width of a Security Header with these field lengths, including the mandatory 2-octet SPI.

func (FieldLengths) Validate

func (fl FieldLengths) Validate() error

Validate reports whether the field widths are usable.

type Mode

type Mode uint8

Mode is the Security Association service type of CCSDS 355.0-B-2 clause 4.2.2.4. Every SA performs one and only one of these.

const (
	// Authentication proves integrity and origin without hiding the data.
	// The algorithm is chosen by AuthAlgorithm: AES-CMAC for the clause E2
	// telecommand baseline, or GMAC as the authentication-only companion to
	// the AES-GCM baselines of clause E1/clause E3/clause E4.
	Authentication Mode = iota + 1

	// Encryption hides the data without authenticating it. Clause 2.3.3 warns that
	// encryption without authentication can give a false sense of security,
	// and this package does not implement it: ApplySecurity returns
	// ErrUnsupportedMode.
	Encryption

	// AuthenticatedEncryption is the clause E1/clause E3/clause E4 baseline: AES-256-GCM with a
	// 96-bit IV and a 128-bit MAC.
	AuthenticatedEncryption
)

func (Mode) String

func (m Mode) String() string

String names the mode.

type SALookup

type SALookup func(spi uint16) (*SecurityAssociation, error)

SALookup returns the Security Association registered for an SPI. It should return ErrUnknownSPI when the index is not configured.

func StaticLookup

func StaticLookup(sas ...*SecurityAssociation) SALookup

StaticLookup builds an SALookup over a fixed set of Security Associations, keyed by their SPI. It is the common case: SAs preloaded before a mission starts (clause 2.3.1.5).

type SecurityAssociation

type SecurityAssociation struct {
	// SPI identifies this SA on the wire (clause 4.2.2.3). 0 and 65535 are reserved.
	SPI uint16

	// Mode is the single service type this SA performs (clause 4.2.2.4).
	Mode Mode

	// Key is the caller-supplied AES-256 key: exactly 32 octets. The SA does
	// not copy it, load it, or store it anywhere; key management is out of
	// scope for this package.
	Key []byte

	// AuthAlgorithm selects the MAC algorithm when Mode is Authentication. It
	// is ignored for AuthenticatedEncryption, which is always AES-GCM.
	AuthAlgorithm AuthAlgorithm

	// FieldLengths is the wire layout of this SA's header and trailer fields.
	FieldLengths FieldLengths

	// AuthMask is the authentication bit mask of clause 4.2.2.6.2, applied with a
	// bitwise AND to the frame header and security header before the MAC is
	// computed. It must be at least as long as the frame header plus the
	// security header.
	//
	// The mask depends on the frame type; clause 4.2.2.6.2 sets the rules. Use the
	// per-frame-type constructors (BaselineAuthMaskTM, BaselineAuthMaskTC,
	// BaselineAuthMaskAOS, BaselineAuthMaskUSLP) which apply the mandatory
	// exclusions: the TM Master Channel Frame Count, the AOS Frame Header
	// Error Control, the Insert Zone, and the Initialization Vector.
	//
	// A nil mask means "authenticate every octet of the frame header". That
	// is stricter than clause 4.2.2.6.2 requires, and for TM and AOS it violates
	// the mandatory exclusions: a field rewritten downstream (the TM MCFC,
	// the AOS FHEC) would break the MAC at the receiver. Leave the mask nil
	// only when the frame header you pass contains no such field. The
	// Initialization Vector is excluded from the MAC either way:
	// Clause 4.2.2.6.2 h) makes that mandatory, so this package enforces it
	// regardless of the mask supplied.
	AuthMask []byte

	// Channels lists the channels this SA is bound to, as clause 4.2.2.2 requires
	// an SA to serve an agreed set of Global Virtual Channels or Global MAP
	// IDs. ProcessSecurityForChannel refuses a frame whose channel is not in
	// the list (clause 4.2.4.3). An empty list declares no binding: the check is
	// skipped, and plain ProcessSecurity never performs it.
	Channels []ChannelID

	// SeqWindow is the sequence number window of clause 2.3.2.3.3: a positive delta
	// beyond which a received counter is discarded. Zero disables the
	// anti-replay check entirely, which is only appropriate for testing.
	SeqWindow uint64
	// contains filtered or unexported fields
}

SecurityAssociation holds the agreed parameters for one secured Virtual Channel or MAP (clause 4.2.2). Both ends configure a matching SA before the link opens; the SPI travels on the wire so the receiver can find it.

A SecurityAssociation is NOT safe for concurrent use. It carries the sender's IV counter and the receiver's anti-replay state, both of which mutate on every frame. Callers must serialize access, typically by giving each direction of each channel its own SA value.

func (*SecurityAssociation) ApplySecurity

func (sa *SecurityAssociation) ApplySecurity(frameHeader, plaintext []byte) ([]byte, error)

ApplySecurity protects one frame data field and returns the bytes to place in the carrier frame, per CCSDS 355.0-B-2 clause 4.2.3:

Security Header || data (ciphertext or plaintext) || Security Trailer

frameHeader is the carrier frame's own bytes, from the first octet of the Transfer Frame Primary Header up to where the Security Header begins. SDLS authenticates those octets without encrypting them, subject to the SA's authentication bit mask. Pass nil to authenticate the security header and data alone.

plaintext is the Transfer Frame Data Field to protect.

The SA advances its IV counter on every successful call and refuses to reuse one, returning ErrIVExhausted when the counter space runs out.

This package does not perform block padding: GCM is a stream mode and Clause E1.2 note 2 records that it needs none. The Pad Length field, if the SA declares one, is transmitted as zeros. ProcessSecurity still honors a non-zero Pad Length on receive.

func (*SecurityAssociation) Humanize

func (sa *SecurityAssociation) Humanize() string

Humanize returns a human-readable summary of the Security Association.

func (*SecurityAssociation) Validate

func (sa *SecurityAssociation) Validate() error

Validate checks the SA against the constraints of clause 4.1.1 and clause 4.2.2.

type SecurityHeader

type SecurityHeader struct {
	SPI       uint16
	IV        []byte // clause 4.1.1.3
	SeqNum    []byte // clause 4.1.1.4
	PadLength []byte // clause 4.1.1.5
}

SecurityHeader is the Security Header of clause 4.1.1: a mandatory SPI followed by three optional fields, contiguous and in this order.

func DecodeSecurityHeader

func DecodeSecurityHeader(data []byte, fl FieldLengths) (*SecurityHeader, int, error)

DecodeSecurityHeader parses a Security Header from the front of data using the field widths of the Security Association that the SPI names. It returns the header and the number of octets consumed.

The caller reads the SPI first (it is always the leading 2 octets), looks up the SA, and passes that SA's FieldLengths here.

func ProcessSecurity

func ProcessSecurity(dataField, frameHeader []byte, lookup SALookup) (*SecurityHeader, []byte, error)

ProcessSecurity reverses ApplySecurity, per CCSDS 355.0-B-2 clause 4.2.4.

dataField is the carrier frame's data field: Security Header, then the protected data, then the Security Trailer. frameHeader is the same header prefix that the sender authenticated. lookup resolves the SPI to its SA.

It returns the decoded Security Header and the recovered Transfer Frame Data Field. On any verification failure it returns a nil data field: no partial plaintext ever escapes, per clause 4.2.4.2.3.

GCM tag comparison is left to crypto/cipher's Open, which is constant time; the CMAC path compares in constant time with crypto/subtle.

ProcessSecurity verifies the SPI only. It has no way to know which channel the frame arrived on, so the clause 4.2.4.3 check that the SA is the one agreed for that channel is left to the caller. Use ProcessSecurityForChannel to have this package enforce it.

func ProcessSecurityForChannel

func ProcessSecurityForChannel(dataField, frameHeader []byte, ch ChannelID, lookup SALookup) (*SecurityHeader, []byte, error)

ProcessSecurityForChannel is ProcessSecurity with the receiving channel in hand: ch identifies the Global Virtual Channel (or Global MAP) the frame arrived on. When the SA that the SPI names declares a channel binding in its Channels list, the frame is rejected with ErrSAChannelMismatch unless ch is in that list (the SA verification of clause 4.2.4.3) before any cryptographic work. An SA with an empty Channels list accepts any channel.

func (*SecurityHeader) Encode

func (h *SecurityHeader) Encode() ([]byte, error)

Encode serializes the Security Header. Field widths come from the values already stored on the struct, so build it with the SA's FieldLengths.

func (*SecurityHeader) Humanize

func (h *SecurityHeader) Humanize() string

Humanize returns a human-readable summary of the Security Header.

func (*SecurityHeader) PadCount

func (h *SecurityHeader) PadCount() int

PadCount returns the Pad Length field as an integer. An absent field means no padding.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL