@@ -18,7 +18,7 @@ var castagnoliTable = crc32.MakeTable(crc32.Castagnoli) // nolint:gochecknogloba
1818var fourZeroes [4 ]byte // nolint:gochecknoglobals
1919
2020/*
21- Packet represents an SCTP packet, defined in https://tools.ietf.org/html/rfc4960 #section-3
21+ Packet represents an SCTP packet, defined in https://tools.ietf.org/html/rfc9260 #section-3
2222An SCTP packet is composed of a common header and chunks. A chunk
2323contains either control information or user data.
2424
@@ -39,7 +39,7 @@ contains either control information or user data.
3939 0 1 2 3
4040 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
4141 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
42- | Source Value Number | Destination Value Number |
42+ | Source Port Number | Destination Port Number |
4343 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
4444 | Verification Tag |
4545 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
@@ -65,27 +65,39 @@ var (
6565 ErrChecksumMismatch = errors .New ("checksum mismatch theirs" )
6666)
6767
68+ // unmarshal parses an SCTP packet and verifies its checksum.
69+ //
70+ // RFC 9260 section 6.8: every packet MUST be protected with CRC32c; receivers verify it.
71+ // RFC 9653 adds an extension (ZCA) that allows a receiver to accept a packet
72+ // whose checksum is incorrect but equal to zero, when an alternate error
73+ // detection method is active for the path (ex: SCTP-over-DTLS) and ZCA has
74+ // been negotiated.
75+ //
76+ // doChecksum indicates whether ZCA is active for the *receiving path*:
77+ // - doChecksum == true : accept a packet if the only checksum issue is that
78+ // it is exactly zero (ZCA + path constraints satisfied).
79+ // - doChecksum == false : strict 9260 verification; any checksum mismatch fails.
80+ //
81+ // Higher layers MUST only pass doChecksum=true if the peer advertised ZCA
82+ // in INIT/INIT-ACK and the current path meets the method’s constraints
83+ // (ex: DTLS). This function does not relax sender-side rules: packets that
84+ // contain INIT or COOKIE ECHO MUST still carry a correct CRC32c (enforced in marshal()).
85+ //
86+ // See RFC 9653 section 5.1 and section 5.3.
6887func (p * packet ) unmarshal (doChecksum bool , raw []byte ) error { //nolint:cyclop
6988 if len (raw ) < packetHeaderSize {
7089 return fmt .Errorf ("%w: raw only %d bytes, %d is the minimum length" , ErrPacketRawTooSmall , len (raw ), packetHeaderSize )
7190 }
7291
7392 offset := packetHeaderSize
7493
75- // Check if doing CRC32c is required.
76- // Without having SCTP AUTH implemented, this depends only on the type
77- // og the first chunk.
78- if offset + chunkHeaderSize <= len (raw ) {
79- switch chunkType (raw [offset ]) {
80- case ctInit , ctCookieEcho :
81- doChecksum = true
82- default :
83- }
84- }
94+ // always compute CRC32c (RFC 9260 section 6.8).
8595 theirChecksum := binary .LittleEndian .Uint32 (raw [8 :])
86- if theirChecksum != 0 || doChecksum {
87- ourChecksum := generatePacketChecksum (raw )
88- if theirChecksum != ourChecksum {
96+ ourChecksum := generatePacketChecksum (raw )
97+ if theirChecksum != ourChecksum {
98+ // RFC 9653: if (a) checksum is zero and (b) caller indicates ZCA is active
99+ // and method constraints are met, accept; otherwise error.
100+ if ! doChecksum || theirChecksum != 0 {
89101 return fmt .Errorf ("%w: %d ours: %d" , ErrChecksumMismatch , theirChecksum , ourChecksum )
90102 }
91103 }
@@ -148,6 +160,16 @@ func (p *packet) unmarshal(doChecksum bool, raw []byte) error { //nolint:cyclop
148160 return nil
149161}
150162
163+ // marshal builds an SCTP packet.
164+ //
165+ // If doChecksum == true, a zero checksum is written (RFC 9653) unless
166+ // the packet contains a restricted chunk (INIT or COOKIE ECHO), in which case
167+ // a correct CRC32c is always written (RFC 9653 section 5.2). If doChecksum == false,
168+ // a correct CRC32c is always written (RFC 9260 section 6.8).
169+ //
170+ // The caller sets doChecksum=true only when the peer has advertised ZCA and
171+ // the current path satisfies the alternate method’s constraints (e.g., DTLS).
172+ // For OOTB responses and other control paths, always marshal with doChecksum=false.
151173func (p * packet ) marshal (doChecksum bool ) ([]byte , error ) {
152174 raw := make ([]byte , packetHeaderSize )
153175
@@ -171,19 +193,27 @@ func (p *packet) marshal(doChecksum bool) ([]byte, error) {
171193 }
172194 }
173195
174- if doChecksum {
175- // golang CRC32C uses reflected input and reflected output, the
176- // net result of this is to have the bytes flipped compared to
177- // the non reflected variant that the spec expects.
178- //
179- // Use LittleEndian.PutUint32 to avoid flipping the bytes in to
180- // the spec compliant checksum order
196+ if doChecksum && ! hasRestrictedChunk (p .chunks ) {
197+ binary .LittleEndian .PutUint32 (raw [8 :], 0 )
198+ } else {
181199 binary .LittleEndian .PutUint32 (raw [8 :], generatePacketChecksum (raw ))
182200 }
183201
184202 return raw , nil
185203}
186204
205+ // restrictedChunks per RFC 9653 section 5.2: INIT and COOKIE ECHO.
206+ func hasRestrictedChunk (chs []chunk ) bool {
207+ for _ , c := range chs {
208+ switch c .(type ) {
209+ case * chunkInit , * chunkCookieEcho :
210+ return true
211+ }
212+ }
213+
214+ return false
215+ }
216+
187217func generatePacketChecksum (raw []byte ) (sum uint32 ) {
188218 // Fastest way to do a crc32 without allocating.
189219 sum = crc32 .Update (sum , castagnoliTable , raw [0 :8 ])
@@ -215,11 +245,13 @@ func (p *packet) String() string {
215245// TryMarshalUnmarshal attempts to marshal and unmarshal a message. Added for fuzzing.
216246func TryMarshalUnmarshal (msg []byte ) int {
217247 p := & packet {}
248+ // Strict mode first (RFC 9260): require valid CRC32c.
218249 err := p .unmarshal (false , msg )
219250 if err != nil {
220251 return 0
221252 }
222253
254+ // Strict send (emit CRC32c).
223255 _ , err = p .marshal (false )
224256 if err != nil {
225257 return 0
0 commit comments