brynja_protocol/tls/
content_type.rs1use brynja_core::ProtocolVersion;
4
5use super::RecordError;
6
7pub const HEARTBEAT_EXTENSION_TYPE: u16 = 15;
9
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12#[non_exhaustive]
13pub enum ContentType {
14 ChangeCipherSpec,
16 Alert,
18 Handshake,
20 ApplicationData,
22 Heartbeat,
24 Tls12Cid,
26 Ack,
28}
29
30#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
32pub enum ContentTypeClass {
33 Assigned(ContentType),
35 Unassigned,
37}
38
39#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48pub struct ContentTypeCode(u8);
49
50#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
61pub struct WirePolicy {
62 version: ProtocolVersion,
63}
64
65impl ContentType {
66 #[must_use]
68 pub const fn code(self) -> u8 {
69 match self {
70 Self::ChangeCipherSpec => 20,
71 Self::Alert => 21,
72 Self::Handshake => 22,
73 Self::ApplicationData => 23,
74 Self::Heartbeat => 24,
75 Self::Tls12Cid => 25,
76 Self::Ack => 26,
77 }
78 }
79}
80
81impl ContentTypeCode {
82 #[must_use]
84 pub const fn classify(code: u8) -> Self {
85 Self(code)
86 }
87
88 #[must_use]
90 pub const fn code(self) -> u8 {
91 self.0
92 }
93
94 #[must_use]
96 pub const fn class(self) -> ContentTypeClass {
97 let assigned = match self.0 {
98 20 => Some(ContentType::ChangeCipherSpec),
99 21 => Some(ContentType::Alert),
100 22 => Some(ContentType::Handshake),
101 23 => Some(ContentType::ApplicationData),
102 24 => Some(ContentType::Heartbeat),
103 25 => Some(ContentType::Tls12Cid),
104 26 => Some(ContentType::Ack),
105 _ => None,
106 };
107 match assigned {
108 Some(content_type) => ContentTypeClass::Assigned(content_type),
109 None => ContentTypeClass::Unassigned,
110 }
111 }
112}
113
114impl WirePolicy {
115 #[must_use]
117 pub const fn for_version(version: ProtocolVersion) -> Self {
118 Self { version }
119 }
120
121 #[must_use]
123 pub const fn version(self) -> ProtocolVersion {
124 self.version
125 }
126
127 pub const fn reject_heartbeat_negotiation(
132 self,
133 extension_type: u16,
134 ) -> Result<(), RecordError> {
135 let _ = self;
136 if extension_type == HEARTBEAT_EXTENSION_TYPE {
137 Err(RecordError::HeartbeatRejected)
138 } else {
139 Ok(())
140 }
141 }
142
143 pub fn admit_inner_content_type(
148 self,
149 code: ContentTypeCode,
150 ) -> Result<ContentType, RecordError> {
151 let content_type = assigned(code)?;
152 if matches!(content_type, ContentType::Heartbeat) {
153 return Err(RecordError::HeartbeatRejected);
154 }
155 let admitted = match self.version {
156 ProtocolVersion::Tls13 => matches!(
157 content_type,
158 ContentType::Alert | ContentType::Handshake | ContentType::ApplicationData
159 ),
160 ProtocolVersion::Dtls13 => matches!(
161 content_type,
162 ContentType::Alert
163 | ContentType::Handshake
164 | ContentType::ApplicationData
165 | ContentType::Ack
166 ),
167 _ => return Err(RecordError::ProfileMismatch),
168 };
169 if admitted {
170 Ok(content_type)
171 } else {
172 Err(RecordError::UnsupportedContentType)
173 }
174 }
175
176 pub(crate) fn admit_plaintext(self, code: ContentTypeCode) -> Result<ContentType, RecordError> {
177 let content_type = assigned(code)?;
178 if matches!(content_type, ContentType::Heartbeat) {
179 return Err(RecordError::HeartbeatRejected);
180 }
181 let admitted = match self.version {
182 ProtocolVersion::Tls12 => matches!(
183 content_type,
184 ContentType::ChangeCipherSpec
185 | ContentType::Alert
186 | ContentType::Handshake
187 | ContentType::ApplicationData
188 ),
189 ProtocolVersion::Tls13 => {
190 if matches!(content_type, ContentType::ApplicationData) {
191 return Err(RecordError::UnprotectedApplicationData);
192 }
193 matches!(
194 content_type,
195 ContentType::ChangeCipherSpec | ContentType::Alert | ContentType::Handshake
196 )
197 }
198 ProtocolVersion::Dtls12 => matches!(
199 content_type,
200 ContentType::ChangeCipherSpec
201 | ContentType::Alert
202 | ContentType::Handshake
203 | ContentType::ApplicationData
204 ),
205 ProtocolVersion::Dtls13 => matches!(
206 content_type,
207 ContentType::Alert | ContentType::Handshake | ContentType::Ack
208 ),
209 _ => false,
210 };
211 if admitted {
212 Ok(content_type)
213 } else {
214 Err(RecordError::UnsupportedContentType)
215 }
216 }
217
218 pub(crate) fn admit_ciphertext(
219 self,
220 code: ContentTypeCode,
221 ) -> Result<ContentType, RecordError> {
222 if matches!(
223 code.class(),
224 ContentTypeClass::Assigned(ContentType::Heartbeat)
225 ) {
226 return Err(RecordError::HeartbeatRejected);
227 }
228 match self.version {
229 ProtocolVersion::Tls13 => {
230 if code.code() == ContentType::ApplicationData.code() {
231 Ok(ContentType::ApplicationData)
232 } else {
233 Err(RecordError::InvalidCiphertextType)
234 }
235 }
236 ProtocolVersion::Tls12 | ProtocolVersion::Dtls12 => self.admit_plaintext(code),
237 ProtocolVersion::Dtls13 => Err(RecordError::ProfileMismatch),
238 _ => Err(RecordError::ProfileMismatch),
239 }
240 }
241}
242
243fn assigned(code: ContentTypeCode) -> Result<ContentType, RecordError> {
244 match code.class() {
245 ContentTypeClass::Assigned(content_type) => Ok(content_type),
246 ContentTypeClass::Unassigned => Err(RecordError::UnsupportedContentType),
247 }
248}