Skip to main content

tlsn_core/transcript/tls/
builder.rs

1//! Builder type for [`TlsTranscript`].
2
3use const_oid::db::rfc5912;
4use rustls_pki_types as pki_types;
5use sha2::{Digest, Sha256};
6use spki::der::{Decode, oid::ObjectIdentifier};
7use tls_core::msgs::{
8    codec::Reader,
9    enums::{
10        ContentType as TlsContentType, HandshakeType, NamedGroup, ProtocolVersion, SignatureScheme,
11    },
12    handshake::{HandshakeMessagePayload, HandshakePayload, KeyExchangeAlgorithm},
13    message::OpaqueMessage,
14};
15
16use crate::{
17    connection::{
18        CertBinding, CertBindingV1_2, HandshakeData, KeyType, ServerEphemKey, ServerSignature,
19        SignatureAlgorithm, TlsVersion,
20    },
21    webpki::CertificateDer,
22};
23
24use super::{ContentType, Record, TlsTranscript, TlsTranscriptError};
25
26/// Builder for [`TlsTranscript`].
27#[derive(Debug, Default)]
28pub struct TlsTranscriptBuilder<'a> {
29    time: Option<u64>,
30    version: Option<TlsVersion>,
31    tls_sent: Option<&'a [u8]>,
32    tls_recv: Option<&'a [u8]>,
33    app_sent: Option<&'a [u8]>,
34    app_recv: Option<&'a [u8]>,
35    records_sent: Option<Vec<Record>>,
36    records_recv: Option<Vec<Record>>,
37    server_signature: Option<ServerSignature>,
38    server_cert_chain: Option<Vec<CertificateDer>>,
39    certificate_binding: Option<CertBinding>,
40}
41
42impl<'a> TlsTranscriptBuilder<'a> {
43    /// Sets the time.
44    pub fn time(mut self, time: u64) -> Self {
45        self.time = Some(time);
46        self
47    }
48
49    /// Sets the TLS version.
50    pub fn version(mut self, version: TlsVersion) -> Self {
51        self.version = Some(version);
52        self
53    }
54
55    /// Sets the tls data sent.
56    pub fn tls_sent(mut self, data: &'a [u8]) -> Self {
57        self.tls_sent = Some(data);
58        self
59    }
60
61    /// Sets the tls data received.
62    pub fn tls_recv(mut self, recv: &'a [u8]) -> Self {
63        self.tls_recv = Some(recv);
64        self
65    }
66
67    /// Sets the plaintext application data sent.
68    pub fn app_sent(mut self, sent: &'a [u8]) -> Self {
69        self.app_sent = Some(sent);
70        self
71    }
72
73    /// Sets the plaintext application data received.
74    pub fn app_recv(mut self, recv: &'a [u8]) -> Self {
75        self.app_recv = Some(recv);
76        self
77    }
78
79    /// Sets the sent records. First record must be client_finished record.
80    pub fn records_sent(mut self, sent: Vec<Record>) -> Self {
81        self.records_sent = Some(sent);
82        self
83    }
84
85    /// Sets the received records. First record must be the server finished
86    /// record.
87    pub fn records_recv(mut self, recv: Vec<Record>) -> Self {
88        self.records_recv = Some(recv);
89        self
90    }
91
92    /// Sets the server signature.
93    pub fn server_signature(mut self, sig: ServerSignature) -> Self {
94        self.server_signature = Some(sig);
95        self
96    }
97
98    /// Sets the server certificate chain.
99    pub fn server_cert_chain(mut self, chain: Vec<CertificateDer>) -> Self {
100        self.server_cert_chain = Some(chain);
101        self
102    }
103
104    /// Sets the certificate binding.
105    pub fn certificate_binding(mut self, binding: CertBinding) -> Self {
106        self.certificate_binding = Some(binding);
107        self
108    }
109
110    /// Builds a [`TlsTranscript`].
111    ///
112    /// Prefers available fields, but if missing tries to parse.
113    pub fn build(mut self) -> Result<TlsTranscript, TlsTranscriptError> {
114        let time = self
115            .time
116            .ok_or_else(|| TlsTranscriptError::missing("time"))?;
117
118        let sent_raw = if let Some(tls_sent) = self.tls_sent {
119            Some(parse_raw_records(tls_sent)?)
120        } else {
121            None
122        };
123
124        let recv_raw = if let Some(tls_recv) = self.tls_recv {
125            Some(parse_raw_records(tls_recv)?)
126        } else {
127            None
128        };
129
130        let (cf_hash, session_hash, sf_hash) = if let Some(sent_raw) = &sent_raw
131            && let Some(recv_raw) = &recv_raw
132        {
133            let (cf_hash, session_hash, sf_hash) =
134                self.parse_handshake_components(sent_raw, recv_raw)?;
135            (Some(cf_hash), Some(session_hash), Some(sf_hash))
136        } else {
137            (None, None, None)
138        };
139
140        let sent = if let Some(records_sent) = self.records_sent {
141            let client_finished = records_sent
142                .first()
143                .expect("client finished record should be available");
144            validate_finished_record(client_finished)?;
145            records_sent
146        } else if let Some(sent_raw) = sent_raw {
147            parse_records(&sent_raw, self.app_sent)?
148        } else {
149            return Err(TlsTranscriptError::missing("sent records"));
150        };
151        validate_seq(&sent)?;
152
153        let recv = if let Some(records_recv) = self.records_recv {
154            let server_finished = records_recv
155                .first()
156                .expect("server finished record should be available");
157            validate_finished_record(server_finished)?;
158            records_recv
159        } else if let Some(recv_raw) = recv_raw {
160            parse_records(&recv_raw, self.app_recv)?
161        } else {
162            return Err(TlsTranscriptError::missing("recv records"));
163        };
164        validate_seq(&recv)?;
165
166        let version = self
167            .version
168            .ok_or_else(|| TlsTranscriptError::missing("version"))?;
169        let certificate_binding = self
170            .certificate_binding
171            .ok_or_else(|| TlsTranscriptError::missing("certificate binding"))?;
172
173        let transcript = TlsTranscript {
174            time,
175            version,
176            server_signature: self.server_signature,
177            server_cert_chain: self.server_cert_chain,
178            certificate_binding,
179            cf_hash,
180            session_hash,
181            sf_hash,
182            sent,
183            recv,
184        };
185
186        Ok(transcript)
187    }
188
189    /// Parse the pre-CCS handshake from both directions.
190    fn parse_handshake_components(
191        &mut self,
192        sent_records: &[OpaqueMessage],
193        recv_records: &[OpaqueMessage],
194    ) -> Result<([u8; 32], [u8; 32], SfHashInput), TlsTranscriptError> {
195        let (sent_hs_bytes, sent_hs) = parse_handshake_stream(sent_records)?;
196        let (recv_hs_bytes, recv_hs) = parse_handshake_stream(recv_records)?;
197
198        let (version, handshake) = extract_handshake(&sent_hs, &recv_hs)?;
199
200        let sent_msgs = scan_handshake_messages(&sent_hs_bytes)?;
201        let recv_msgs = scan_handshake_messages(&recv_hs_bytes)?;
202        let (cf_hash, session_hash, sf_hash_input) =
203            compute_handshake_hashes(sent_hs_bytes, recv_hs_bytes, &sent_msgs, &recv_msgs)?;
204
205        if self.version.is_none() {
206            self.version = Some(version);
207        }
208        if self.server_signature.is_none() {
209            self.server_signature = Some(handshake.sig);
210        }
211        if self.server_cert_chain.is_none() {
212            self.server_cert_chain = Some(handshake.certs);
213        }
214        if self.certificate_binding.is_none() {
215            self.certificate_binding = Some(handshake.binding);
216        }
217
218        Ok((cf_hash, session_hash, sf_hash_input))
219    }
220}
221
222#[derive(Debug, Clone)]
223pub(crate) struct SfHashInput {
224    pub(crate) sent_hs_bytes: Vec<u8>,
225    pub(crate) recv_hs_bytes: Vec<u8>,
226    pub(crate) sent_ch_end: usize,
227    pub(crate) recv_shd_end: usize,
228}
229
230const NONCE_LEN: usize = 8;
231const TAG_LEN: usize = 16;
232
233/// Parse raw TLS record frames from a byte slice.
234fn parse_raw_records(bytes: &[u8]) -> Result<Vec<OpaqueMessage>, TlsTranscriptError> {
235    let mut reader = Reader::init(bytes);
236    let mut records = Vec::new();
237    while reader.any_left() {
238        let msg = OpaqueMessage::read(&mut reader)
239            .map_err(|e| TlsTranscriptError::parse(format!("failed to read TLS record: {e:?}")))?;
240        records.push(msg);
241    }
242    Ok(records)
243}
244
245/// Collect the pre-CCS handshake byte stream and decode it into
246/// individual handshake messages.
247fn parse_handshake_stream(
248    records: &[OpaqueMessage],
249) -> Result<(Vec<u8>, Vec<HandshakeMessagePayload>), TlsTranscriptError> {
250    let handshake_bytes = collect_handshake_bytes_pre_ccs(records);
251
252    let mut reader = Reader::init(&handshake_bytes);
253    let mut messages = Vec::new();
254    while reader.any_left() {
255        let msg = HandshakeMessagePayload::read_version(&mut reader, ProtocolVersion::TLSv1_2)
256            .ok_or_else(|| TlsTranscriptError::parse("failed to parse handshake message"))?;
257        messages.push(msg);
258    }
259    Ok((handshake_bytes, messages))
260}
261
262/// Validate handshake-message bounds and compute the two
263/// digests `cf_hash` and `session_hash`, and the cached
264/// inputs needed to derive `sf_hash` later from `cf_vd`.
265fn compute_handshake_hashes(
266    sent_hs_bytes: Vec<u8>,
267    recv_hs_bytes: Vec<u8>,
268    sent_msgs: &[HandshakeMsgInfo],
269    recv_msgs: &[HandshakeMsgInfo],
270) -> Result<([u8; 32], [u8; 32], SfHashInput), TlsTranscriptError> {
271    let client_hello = sent_msgs
272        .first()
273        .ok_or_else(|| TlsTranscriptError::parse("missing ClientHello in sent handshake stream"))?;
274    if client_hello.typ != HandshakeType::ClientHello {
275        return Err(TlsTranscriptError::parse(
276            "first sent handshake message is not ClientHello",
277        ));
278    }
279    let ckx = sent_msgs
280        .iter()
281        .find(|m| m.typ == HandshakeType::ClientKeyExchange)
282        .ok_or_else(|| TlsTranscriptError::parse("missing ClientKeyExchange"))?;
283    if !recv_msgs
284        .iter()
285        .any(|m| m.typ == HandshakeType::ServerHello)
286    {
287        return Err(TlsTranscriptError::parse("missing ServerHello"));
288    }
289    let shd = recv_msgs
290        .iter()
291        .find(|m| m.typ == HandshakeType::ServerHelloDone)
292        .ok_or_else(|| TlsTranscriptError::parse("missing ServerHelloDone"))?;
293
294    let sent_ch_end = client_hello.end;
295    let recv_shd_end = shd.end;
296
297    // session_hash: ClientHello → server flight (ServerHello..ServerHelloDone)
298    // → client flight up to and including ClientKeyExchange.
299    let mut hasher = Sha256::new();
300    hasher.update(&sent_hs_bytes[..sent_ch_end]);
301    hasher.update(&recv_hs_bytes);
302    hasher.update(&sent_hs_bytes[sent_ch_end..ckx.end]);
303    let session_hash: [u8; 32] = hasher.finalize().into();
304
305    // cf_hash: ClientHello → server first flight (..ServerHelloDone)
306    // → remaining client flight (ClientKeyExchange and, when client
307    // auth is active, CertificateVerify).
308    let mut hasher = Sha256::new();
309    hasher.update(&sent_hs_bytes[..sent_ch_end]);
310    hasher.update(&recv_hs_bytes[..recv_shd_end]);
311    hasher.update(&sent_hs_bytes[sent_ch_end..]);
312    let cf_hash: [u8; 32] = hasher.finalize().into();
313
314    Ok((
315        cf_hash,
316        session_hash,
317        SfHashInput {
318            sent_hs_bytes,
319            recv_hs_bytes,
320            sent_ch_end,
321            recv_shd_end,
322        },
323    ))
324}
325
326/// Concatenate the payload bytes of all handshake records appearing
327/// before the first ChangeCipherSpec. These are the plaintext
328/// handshake bytes as they appeared on the wire.
329fn collect_handshake_bytes_pre_ccs(records: &[OpaqueMessage]) -> Vec<u8> {
330    let mut handshake_bytes = Vec::new();
331    for record in records {
332        if record.typ == TlsContentType::ChangeCipherSpec {
333            break;
334        }
335        if record.typ == TlsContentType::Handshake {
336            handshake_bytes.extend_from_slice(&record.payload.0);
337        }
338    }
339    handshake_bytes
340}
341
342/// Position information for a single handshake message inside a
343/// concatenated handshake byte stream.
344struct HandshakeMsgInfo {
345    typ: HandshakeType,
346    /// Exclusive end offset of the message (header + body) in the
347    /// scanned byte stream.
348    end: usize,
349}
350
351/// Walk a concatenated handshake byte stream and return the
352/// [`HandshakeMsgInfo`].
353fn scan_handshake_messages(bytes: &[u8]) -> Result<Vec<HandshakeMsgInfo>, TlsTranscriptError> {
354    let mut out = Vec::new();
355    let mut pos = 0;
356    while pos < bytes.len() {
357        if bytes.len() - pos < 4 {
358            return Err(TlsTranscriptError::parse("truncated handshake header"));
359        }
360        let typ = HandshakeType::from(bytes[pos]);
361        let len = u32::from_be_bytes([0, bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]) as usize;
362        let msg_end = pos + 4 + len;
363        if msg_end > bytes.len() {
364            return Err(TlsTranscriptError::parse(
365                "handshake message length overflows buffer",
366            ));
367        }
368        out.push(HandshakeMsgInfo { typ, end: msg_end });
369        pos = msg_end;
370    }
371    Ok(out)
372}
373
374/// Split an encrypted TLS record payload into nonce / ciphertext / tag.
375fn split_into_record(
376    seq: u64,
377    payload: &[u8],
378    typ: TlsContentType,
379) -> Result<Record, TlsTranscriptError> {
380    let typ = ContentType::from(typ);
381
382    if payload.len() < NONCE_LEN + TAG_LEN {
383        return Err(TlsTranscriptError::parse("encrypted record too short"));
384    }
385
386    Ok(Record {
387        seq,
388        typ,
389        plaintext: None,
390        explicit_nonce: payload[..NONCE_LEN].to_vec(),
391        ciphertext: payload[NONCE_LEN..payload.len() - TAG_LEN].to_vec(),
392        tag: Some(payload[payload.len() - TAG_LEN..].to_vec()),
393    })
394}
395
396/// Extract the full handshake data from parsed handshake messages.
397fn extract_handshake(
398    sent_hs: &[HandshakeMessagePayload],
399    recv_hs: &[HandshakeMessagePayload],
400) -> Result<(TlsVersion, HandshakeData), TlsTranscriptError> {
401    let (version, server_random) = extract_server_hello_data(recv_hs)?;
402    let client_random = extract_client_random(sent_hs)?;
403    let certs = extract_certs(recv_hs)?;
404    let (server_ephemeral_key, sig) = extract_server_key_exchange(recv_hs, &certs)?;
405
406    let binding = CertBinding::V1_2(CertBindingV1_2 {
407        client_random,
408        server_random,
409        server_ephemeral_key,
410    });
411
412    let handshake = HandshakeData {
413        certs,
414        sig,
415        binding,
416    };
417
418    Ok((version, handshake))
419}
420
421/// Extract the TLS version and server random from the ServerHello message.
422fn extract_server_hello_data(
423    recv_hs: &[HandshakeMessagePayload],
424) -> Result<(TlsVersion, [u8; 32]), TlsTranscriptError> {
425    let server_hello = recv_hs
426        .iter()
427        .find_map(|msg| match &msg.payload {
428            HandshakePayload::ServerHello(sh) => Some(sh),
429            _ => None,
430        })
431        .ok_or_else(|| TlsTranscriptError::parse("missing ServerHello"))?;
432
433    let version = TlsVersion::try_from(server_hello.legacy_version)
434        .map_err(|e| TlsTranscriptError::parse(format!("unsupported TLS version: {e}")))?;
435
436    Ok((version, server_hello.random.0))
437}
438
439fn extract_client_random(
440    sent_hs: &[HandshakeMessagePayload],
441) -> Result<[u8; 32], TlsTranscriptError> {
442    let client_hello = sent_hs
443        .iter()
444        .find_map(|msg| match &msg.payload {
445            HandshakePayload::ClientHello(ch) => Some(ch),
446            _ => None,
447        })
448        .ok_or_else(|| TlsTranscriptError::parse("missing ClientHello"))?;
449
450    Ok(client_hello.random.0)
451}
452
453fn extract_certs(
454    recv_hs: &[HandshakeMessagePayload],
455) -> Result<Vec<CertificateDer>, TlsTranscriptError> {
456    let cert_payload = recv_hs
457        .iter()
458        .find_map(|msg| match &msg.payload {
459            HandshakePayload::Certificate(certs) => Some(certs),
460            _ => None,
461        })
462        .ok_or_else(|| TlsTranscriptError::parse("missing Certificate"))?;
463
464    Ok(cert_payload
465        .iter()
466        .map(|cert| CertificateDer(cert.0.clone()))
467        .collect())
468}
469
470fn extract_server_key_exchange(
471    recv_hs: &[HandshakeMessagePayload],
472    certs: &[CertificateDer],
473) -> Result<(ServerEphemKey, ServerSignature), TlsTranscriptError> {
474    let ske = recv_hs
475        .iter()
476        .find_map(|msg| match &msg.payload {
477            HandshakePayload::ServerKeyExchange(ske) => Some(ske),
478            _ => None,
479        })
480        .ok_or_else(|| TlsTranscriptError::parse("missing ServerKeyExchange"))?;
481
482    let ecdhe = ske
483        .unwrap_given_kxa(&KeyExchangeAlgorithm::ECDHE)
484        .ok_or_else(|| TlsTranscriptError::parse("failed to parse ECDHE ServerKeyExchange"))?;
485
486    if ecdhe.params.curve_params.named_group != NamedGroup::secp256r1 {
487        return Err(TlsTranscriptError::parse(
488            "unsupported key exchange group (only secp256r1 is supported)",
489        ));
490    }
491
492    let key = ServerEphemKey {
493        typ: KeyType::SECP256R1,
494        key: ecdhe.params.public.0.clone(),
495    };
496
497    let alg = map_signature_scheme(ecdhe.dss.scheme, certs)?;
498    let sig = ServerSignature {
499        alg,
500        sig: ecdhe.dss.sig.0.clone(),
501    };
502
503    Ok((key, sig))
504}
505
506/// Map a TLS `SignatureScheme` to our `SignatureAlgorithm`.
507///
508/// For ECDSA in TLS 1.2 the scheme only specifies the hash, not the curve.
509/// The curve is determined from the end-entity certificate's public key.
510fn map_signature_scheme(
511    scheme: SignatureScheme,
512    certs: &[CertificateDer],
513) -> Result<SignatureAlgorithm, TlsTranscriptError> {
514    match scheme {
515        SignatureScheme::RSA_PKCS1_SHA256 => Ok(SignatureAlgorithm::RSA_PKCS1_2048_8192_SHA256),
516        SignatureScheme::RSA_PKCS1_SHA384 => Ok(SignatureAlgorithm::RSA_PKCS1_2048_8192_SHA384),
517        SignatureScheme::RSA_PKCS1_SHA512 => Ok(SignatureAlgorithm::RSA_PKCS1_2048_8192_SHA512),
518        SignatureScheme::RSA_PSS_SHA256 => {
519            Ok(SignatureAlgorithm::RSA_PSS_2048_8192_SHA256_LEGACY_KEY)
520        }
521        SignatureScheme::RSA_PSS_SHA384 => {
522            Ok(SignatureAlgorithm::RSA_PSS_2048_8192_SHA384_LEGACY_KEY)
523        }
524        SignatureScheme::RSA_PSS_SHA512 => {
525            Ok(SignatureAlgorithm::RSA_PSS_2048_8192_SHA512_LEGACY_KEY)
526        }
527        SignatureScheme::ED25519 => Ok(SignatureAlgorithm::ED25519),
528        // In TLS 1.2, ECDSA schemes specify only the hash — the curve
529        // comes from the server certificate's public key.
530        SignatureScheme::ECDSA_NISTP256_SHA256 => {
531            let curve_oid = extract_ec_curve_oid(certs)?;
532            match curve_oid {
533                oid if oid == rfc5912::SECP_256_R_1 => {
534                    Ok(SignatureAlgorithm::ECDSA_NISTP256_SHA256)
535                }
536                oid if oid == rfc5912::SECP_384_R_1 => {
537                    Ok(SignatureAlgorithm::ECDSA_NISTP384_SHA256)
538                }
539                _ => Err(TlsTranscriptError::parse(format!(
540                    "unsupported EC curve: {curve_oid}"
541                ))),
542            }
543        }
544        SignatureScheme::ECDSA_NISTP384_SHA384 => {
545            let curve_oid = extract_ec_curve_oid(certs)?;
546            match curve_oid {
547                oid if oid == rfc5912::SECP_256_R_1 => {
548                    Ok(SignatureAlgorithm::ECDSA_NISTP256_SHA384)
549                }
550                oid if oid == rfc5912::SECP_384_R_1 => {
551                    Ok(SignatureAlgorithm::ECDSA_NISTP384_SHA384)
552                }
553                _ => Err(TlsTranscriptError::parse(format!(
554                    "unsupported EC curve: {curve_oid}"
555                ))),
556            }
557        }
558        _ => Err(TlsTranscriptError::parse(format!(
559            "unsupported signature scheme: {scheme:?}"
560        ))),
561    }
562}
563
564/// Extract the EC curve OID from the end-entity certificate's SPKI.
565fn extract_ec_curve_oid(certs: &[CertificateDer]) -> Result<ObjectIdentifier, TlsTranscriptError> {
566    let ee_cert = certs
567        .first()
568        .ok_or_else(|| TlsTranscriptError::parse("missing end-entity certificate"))?;
569
570    let cert = pki_types::CertificateDer::from(ee_cert.0.as_slice());
571    let ee = webpki::EndEntityCert::try_from(&cert)
572        .map_err(|e| TlsTranscriptError::parse(format!("invalid end-entity certificate: {e}")))?;
573    let spki_der = ee.subject_public_key_info();
574    let spki = spki::SubjectPublicKeyInfoRef::from_der(spki_der.as_ref())
575        .map_err(|e| TlsTranscriptError::parse(format!("invalid SPKI: {e}")))?;
576    spki.algorithm
577        .parameters
578        .ok_or_else(|| TlsTranscriptError::parse("missing EC curve parameters in SPKI"))?
579        .decode_as::<ObjectIdentifier>()
580        .map_err(|e| TlsTranscriptError::parse(format!("failed to decode EC curve OID: {e}")))
581}
582
583fn validate_finished_record(value: &Record) -> Result<(), TlsTranscriptError> {
584    if !matches!(value.typ, ContentType::Handshake) {
585        return Err(TlsTranscriptError::validation(format!(
586            "first record expected to be a handshake finished message, but has type {:?}",
587            value.typ
588        )));
589    }
590    if value.seq != 0 {
591        return Err(TlsTranscriptError::validation(format!(
592            "first record should have sequence number 0, but has {}",
593            value.seq
594        )));
595    }
596
597    if let Some(payload) = &value.plaintext {
598        let mut reader = Reader::init(payload);
599        let payload = HandshakeMessagePayload::read_version(&mut reader, ProtocolVersion::TLSv1_2)
600            .ok_or_else(|| {
601                TlsTranscriptError::validation("expected finished record but record is malformed")
602            })?;
603
604        if !matches!(payload.payload, HandshakePayload::Finished(_)) {
605            return Err(TlsTranscriptError::validation("expected finished record"));
606        }
607    }
608
609    Ok(())
610}
611
612fn validate_seq(records: &[Record]) -> Result<(), TlsTranscriptError> {
613    for pair in records.windows(2) {
614        if pair[1].seq <= pair[0].seq {
615            return Err(TlsTranscriptError::validation(format!(
616                "records must have strictly increasing sequence numbers, but got {} after {}",
617                pair[1].seq, pair[0].seq,
618            )));
619        }
620    }
621    Ok(())
622}
623
624/// Parses records and adds plaintext to application data records.
625fn parse_records(
626    records: &[OpaqueMessage],
627    app_data: Option<&[u8]>,
628) -> Result<Vec<Record>, TlsTranscriptError> {
629    let mut parsed = Vec::new();
630
631    // Parse finished record.
632    let ccs = records
633        .iter()
634        .position(|r| r.typ == TlsContentType::ChangeCipherSpec)
635        .ok_or_else(|| TlsTranscriptError::missing("ccs record is missing"))?;
636    let raw_finished = records
637        .get(ccs + 1)
638        .ok_or_else(|| TlsTranscriptError::parse("missing Finished record after CCS"))?;
639
640    let payload = &raw_finished.payload.0;
641    if payload.len() < NONCE_LEN + TAG_LEN {
642        return Err(TlsTranscriptError::parse("encrypted record too short"));
643    }
644
645    let typ = raw_finished.typ.into();
646    if !matches!(typ, ContentType::Handshake) {
647        return Err(TlsTranscriptError::validation(format!(
648            "expected content type handshake for finished record, but got {:?}",
649            typ
650        )));
651    }
652
653    let finished_record = Record {
654        seq: 0,
655        typ: ContentType::Handshake,
656        plaintext: None,
657        explicit_nonce: payload[..NONCE_LEN].to_vec(),
658        ciphertext: payload[NONCE_LEN..payload.len() - TAG_LEN].to_vec(),
659        tag: Some(payload[payload.len() - TAG_LEN..].to_vec()),
660    };
661    parsed.push(finished_record);
662
663    // Now parse app data and skip CCS and the Finished record.
664    let mut consumed = 0;
665    for (seq, record) in (1u64..).zip(records.iter().skip(ccs + 2)) {
666        let mut rec = split_into_record(seq, &record.payload.0, record.typ)?;
667
668        if rec.typ == ContentType::ApplicationData
669            && let Some(app_data) = app_data
670        {
671            let cipher_len = rec.ciphertext.len();
672            if app_data[consumed..].len() >= cipher_len {
673                rec.plaintext = Some(app_data[consumed..consumed + cipher_len].to_vec());
674                consumed += cipher_len;
675            } else {
676                return Err(TlsTranscriptError::parse(
677                    "insufficient plaintext application data",
678                ));
679            }
680        }
681        parsed.push(rec);
682    }
683
684    Ok(parsed)
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690    use tls_server_fixture::SERVER_CERT_DER;
691
692    // Pre-generated TLS 1.2 transcript fixtures. Captured once from a real
693    // handshake against `tls_server_fixture::bind_test_server` followed by
694    // four `msgN` records (each padded to 1024 bytes) echoed as "hello".
695    // The server certificate is `tls_server_fixture::SERVER_CERT_DER`, so
696    // regenerate these files if that cert ever changes.
697    const SENT: &[u8] = include_bytes!("../fixtures/tls_sent.bin");
698    const RECV: &[u8] = include_bytes!("../fixtures/tls_recv.bin");
699    const APP_SENT: &[u8] = include_bytes!("../fixtures/tls_app_sent.bin");
700    const APP_RECV: &[u8] = include_bytes!("../fixtures/tls_app_recv.bin");
701    const MSG_COUNT: usize = 4;
702
703    #[test]
704    fn test_parse_handshake() {
705        let transcript = TlsTranscript::builder()
706            .time(0)
707            .tls_sent(SENT)
708            .tls_recv(RECV)
709            .build()
710            .unwrap();
711
712        assert_eq!(transcript.version(), TlsVersion::V1_2);
713
714        // Certificate chain should contain the server cert.
715        let certs = transcript.server_cert_chain().expect("cert chain parsed");
716        assert_eq!(certs[0].0, SERVER_CERT_DER);
717
718        // Signature algorithm should be an RSA variant.
719        let alg = &transcript.server_signature().expect("signature parsed").alg;
720        assert!(
721            matches!(
722                alg,
723                SignatureAlgorithm::RSA_PKCS1_2048_8192_SHA256
724                    | SignatureAlgorithm::RSA_PKCS1_2048_8192_SHA384
725                    | SignatureAlgorithm::RSA_PKCS1_2048_8192_SHA512
726                    | SignatureAlgorithm::RSA_PSS_2048_8192_SHA256_LEGACY_KEY
727                    | SignatureAlgorithm::RSA_PSS_2048_8192_SHA384_LEGACY_KEY
728                    | SignatureAlgorithm::RSA_PSS_2048_8192_SHA512_LEGACY_KEY
729            ),
730            "expected RSA signature algorithm, got {:?}",
731            alg
732        );
733
734        // CertBinding should be V1_2 with valid values.
735        let CertBinding::V1_2(binding) = transcript.certificate_binding();
736
737        assert_ne!(binding.client_random, [0u8; 32]);
738        assert_ne!(binding.server_random, [0u8; 32]);
739        assert_eq!(binding.server_ephemeral_key.typ, KeyType::SECP256R1);
740        // Uncompressed EC point: 65 bytes, starts with 0x04.
741        assert_eq!(binding.server_ephemeral_key.key.len(), 65);
742        assert_eq!(binding.server_ephemeral_key.key[0], 0x04);
743    }
744
745    #[test]
746    fn test_parse_app_records() {
747        let sent_raw = parse_raw_records(SENT).unwrap();
748        let recv_raw = parse_raw_records(RECV).unwrap();
749        let sent_records = parse_records(&sent_raw, Some(APP_SENT)).unwrap();
750        let recv_records = parse_records(&recv_raw, Some(APP_RECV)).unwrap();
751
752        // Sent records: 5 messages, finshed record and 4 app records
753        assert_eq!(sent_records.len(), MSG_COUNT + 1);
754        for (i, record) in sent_records.iter().enumerate() {
755            let expected_seq = (i) as u64;
756            if i == 0 {
757                assert_eq!(record.typ, ContentType::Handshake);
758            } else {
759                assert_eq!(record.typ, ContentType::ApplicationData);
760            }
761            assert_eq!(record.seq, expected_seq);
762            assert_eq!(record.explicit_nonce.len(), 8);
763            assert!(!record.ciphertext.is_empty());
764            assert_eq!(record.tag.as_ref().unwrap().len(), 16);
765        }
766
767        // Recv records: 5 messages, finshed record and 4 app records
768        assert_eq!(recv_records.len(), MSG_COUNT + 1);
769        for (i, record) in recv_records.iter().enumerate() {
770            let expected_seq = (i) as u64;
771            assert_eq!(record.seq, expected_seq);
772            if i == 0 {
773                assert_eq!(record.typ, ContentType::Handshake);
774            } else {
775                assert_eq!(record.typ, ContentType::ApplicationData);
776            }
777            assert_eq!(record.explicit_nonce.len(), 8);
778            assert!(!record.ciphertext.is_empty());
779            assert_eq!(record.tag.as_ref().unwrap().len(), 16);
780        }
781    }
782
783    #[test]
784    fn test_parse_into_transcript() {
785        let transcript = TlsTranscript::builder()
786            .time(0)
787            .tls_sent(SENT)
788            .tls_recv(RECV)
789            .app_sent(APP_SENT)
790            .app_recv(APP_RECV)
791            .build()
792            .unwrap();
793
794        assert_eq!(transcript.version(), TlsVersion::V1_2);
795        assert!(transcript.server_cert_chain().is_some());
796        assert!(transcript.server_signature().is_some());
797
798        // Finished verify-data records have content type Handshake.
799        assert!(!transcript.client_finished().ciphertext.is_empty());
800        assert!(!transcript.server_finished().ciphertext.is_empty());
801
802        // 1 finished record and 4 app data records each
803        assert_eq!(transcript.sent().len(), MSG_COUNT + 1);
804        assert_eq!(transcript.recv().len(), MSG_COUNT + 1);
805        assert_eq!(transcript.sent()[0].seq, 0);
806        assert_eq!(transcript.recv()[0].seq, 0);
807    }
808}