tlsn_core/transcript/
tls.rs1use crate::{
4 connection::{CertBinding, ServerSignature, TlsVersion},
5 transcript::{Direction, Transcript, tls::builder::SfHashInput},
6 webpki::CertificateDer,
7};
8
9use sha2::{Digest, Sha256};
10
11mod builder;
12pub use builder::TlsTranscriptBuilder;
13
14#[derive(Debug, Clone)]
25pub struct TlsTranscript {
26 pub(crate) time: u64,
27 pub(crate) version: TlsVersion,
28 pub(crate) server_signature: Option<ServerSignature>,
29 pub(crate) server_cert_chain: Option<Vec<CertificateDer>>,
30 pub(crate) certificate_binding: CertBinding,
31 pub(crate) sent: Vec<Record>,
32 pub(crate) recv: Vec<Record>,
33 pub(crate) cf_hash: Option<[u8; 32]>,
34 pub(crate) session_hash: Option<[u8; 32]>,
35 pub(crate) sf_hash: Option<SfHashInput>,
36}
37
38impl TlsTranscript {
39 pub fn builder<'a>() -> TlsTranscriptBuilder<'a> {
41 TlsTranscriptBuilder::default()
42 }
43
44 pub fn time(&self) -> u64 {
46 self.time
47 }
48
49 pub fn version(&self) -> TlsVersion {
51 self.version
52 }
53
54 pub fn server_signature(&self) -> Option<&ServerSignature> {
56 self.server_signature.as_ref()
57 }
58
59 pub fn server_cert_chain(&self) -> Option<&[CertificateDer]> {
61 self.server_cert_chain.as_deref()
62 }
63
64 pub fn certificate_binding(&self) -> &CertBinding {
66 &self.certificate_binding
67 }
68
69 pub fn sent(&self) -> &[Record] {
71 &self.sent
72 }
73
74 pub fn recv(&self) -> &[Record] {
76 &self.recv
77 }
78
79 pub fn client_finished(&self) -> &Record {
81 self.sent()
82 .first()
83 .expect("client finished record should be present")
84 }
85
86 pub fn cf_vd(&self) -> Option<&[u8]> {
88 let cf = self.client_finished();
89
90 cf.plaintext.as_ref().and_then(|plain| plain.get(4..))
92 }
93
94 pub fn server_finished(&self) -> &Record {
96 self.recv()
97 .first()
98 .expect("server finished record should be present")
99 }
100
101 pub fn sf_vd(&self) -> Option<&[u8]> {
103 let sf = self.server_finished();
104
105 sf.plaintext.as_ref().and_then(|plain| plain.get(4..))
107 }
108
109 pub fn cf_hash(&self) -> Option<[u8; 32]> {
111 self.cf_hash.as_ref().copied()
112 }
113
114 pub fn session_hash(&self) -> Option<[u8; 32]> {
120 self.session_hash.as_ref().copied()
121 }
122
123 pub fn sf_hash(&self, cf_vd: &[u8; 12]) -> Option<[u8; 32]> {
126 let sf_hash = self.sf_hash.as_ref()?;
127 let SfHashInput {
128 sent_hs_bytes,
129 recv_hs_bytes,
130 sent_ch_end,
131 recv_shd_end,
132 } = sf_hash;
133
134 let mut hasher = Sha256::new();
135 hasher.update(&sent_hs_bytes[..*sent_ch_end]);
136 hasher.update(&recv_hs_bytes[..*recv_shd_end]);
137 hasher.update(&sent_hs_bytes[*sent_ch_end..]);
138
139 hasher.update([0x14, 0x00, 0x00, 0x0c]);
141 hasher.update(cf_vd);
142 hasher.update(&recv_hs_bytes[*recv_shd_end..]);
143
144 let sf_hash = hasher.finalize().into();
145 Some(sf_hash)
146 }
147
148 pub fn to_transcript(&self) -> Result<Transcript, TlsTranscriptError> {
150 let mut sent = Vec::new();
151 let mut recv = Vec::new();
152
153 for record in self
154 .sent
155 .iter()
156 .filter(|record| record.typ == ContentType::ApplicationData)
157 {
158 let plaintext = record
159 .plaintext
160 .as_ref()
161 .ok_or(ErrorRepr::Incomplete {
162 direction: Direction::Sent,
163 seq: record.seq,
164 })?
165 .clone();
166 sent.extend_from_slice(&plaintext);
167 }
168
169 for record in self
170 .recv
171 .iter()
172 .filter(|record| record.typ == ContentType::ApplicationData)
173 {
174 let plaintext = record
175 .plaintext
176 .as_ref()
177 .ok_or(ErrorRepr::Incomplete {
178 direction: Direction::Received,
179 seq: record.seq,
180 })?
181 .clone();
182 recv.extend_from_slice(&plaintext);
183 }
184
185 Ok(Transcript::new(sent, recv))
186 }
187}
188
189#[derive(Clone)]
191pub struct Record {
192 pub seq: u64,
194 pub typ: ContentType,
196 pub plaintext: Option<Vec<u8>>,
198 pub explicit_nonce: Vec<u8>,
200 pub ciphertext: Vec<u8>,
202 pub tag: Option<Vec<u8>>,
204}
205
206opaque_debug::implement!(Record);
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
210pub enum ContentType {
211 ChangeCipherSpec,
213 Alert,
215 Handshake,
217 ApplicationData,
219 Heartbeat,
221 Unknown(u8),
223}
224
225impl From<ContentType> for tls_core::msgs::enums::ContentType {
226 fn from(content_type: ContentType) -> Self {
227 match content_type {
228 ContentType::ChangeCipherSpec => tls_core::msgs::enums::ContentType::ChangeCipherSpec,
229 ContentType::Alert => tls_core::msgs::enums::ContentType::Alert,
230 ContentType::Handshake => tls_core::msgs::enums::ContentType::Handshake,
231 ContentType::ApplicationData => tls_core::msgs::enums::ContentType::ApplicationData,
232 ContentType::Heartbeat => tls_core::msgs::enums::ContentType::Heartbeat,
233 ContentType::Unknown(id) => tls_core::msgs::enums::ContentType::Unknown(id),
234 }
235 }
236}
237
238impl From<tls_core::msgs::enums::ContentType> for ContentType {
239 fn from(content_type: tls_core::msgs::enums::ContentType) -> Self {
240 match content_type {
241 tls_core::msgs::enums::ContentType::ChangeCipherSpec => ContentType::ChangeCipherSpec,
242 tls_core::msgs::enums::ContentType::Alert => ContentType::Alert,
243 tls_core::msgs::enums::ContentType::Handshake => ContentType::Handshake,
244 tls_core::msgs::enums::ContentType::ApplicationData => ContentType::ApplicationData,
245 tls_core::msgs::enums::ContentType::Heartbeat => ContentType::Heartbeat,
246 tls_core::msgs::enums::ContentType::Unknown(id) => ContentType::Unknown(id),
247 }
248 }
249}
250
251#[derive(Debug, thiserror::Error)]
253#[error("TLS transcript error: {0}")]
254pub struct TlsTranscriptError(#[from] ErrorRepr);
255
256impl TlsTranscriptError {
257 fn parse(msg: impl Into<String>) -> Self {
258 Self(ErrorRepr::Parse(msg.into()))
259 }
260
261 fn missing(field: &'static str) -> Self {
262 Self(ErrorRepr::Missing(field))
263 }
264
265 fn validation(msg: impl Into<String>) -> Self {
266 Self(ErrorRepr::Validation(msg.into()))
267 }
268}
269
270#[derive(Debug, thiserror::Error)]
271pub(crate) enum ErrorRepr {
272 #[error("parse error: {0}")]
273 Parse(String),
274 #[error("missing field: {0}")]
275 Missing(&'static str),
276 #[error("incomplete transcript ({direction}): seq {seq}")]
277 Incomplete { direction: Direction, seq: u64 },
278 #[error("validation error: {0}")]
279 Validation(String),
280}