tlsn_core/attestation/
builder.rs

1use std::error::Error;
2
3use rand::{rng, Rng};
4
5use crate::{
6    attestation::{
7        Attestation, AttestationConfig, Body, EncodingCommitment, Extension, FieldId, FieldKind,
8        Header, ServerCertCommitment, VERSION,
9    },
10    connection::{ConnectionInfo, ServerEphemKey},
11    hash::{HashAlgId, TypedHash},
12    request::Request,
13    serialize::CanonicalSerialize,
14    signing::SignatureAlgId,
15    transcript::encoding::EncoderSecret,
16    CryptoProvider,
17};
18
19/// Attestation builder state for accepting a request.
20#[derive(Debug)]
21pub struct Accept {}
22
23#[derive(Debug)]
24pub struct Sign {
25    signature_alg: SignatureAlgId,
26    hash_alg: HashAlgId,
27    connection_info: Option<ConnectionInfo>,
28    server_ephemeral_key: Option<ServerEphemKey>,
29    cert_commitment: ServerCertCommitment,
30    encoding_commitment_root: Option<TypedHash>,
31    encoder_secret: Option<EncoderSecret>,
32    extensions: Vec<Extension>,
33}
34
35/// An attestation builder.
36#[derive(Debug)]
37pub struct AttestationBuilder<'a, T = Accept> {
38    config: &'a AttestationConfig,
39    state: T,
40}
41
42impl<'a> AttestationBuilder<'a, Accept> {
43    /// Creates a new attestation builder.
44    pub fn new(config: &'a AttestationConfig) -> Self {
45        Self {
46            config,
47            state: Accept {},
48        }
49    }
50
51    /// Accepts the attestation request.
52    pub fn accept_request(
53        self,
54        request: Request,
55    ) -> Result<AttestationBuilder<'a, Sign>, AttestationBuilderError> {
56        let config = self.config;
57
58        let Request {
59            signature_alg,
60            hash_alg,
61            server_cert_commitment: cert_commitment,
62            encoding_commitment_root,
63            extensions,
64        } = request;
65
66        if !config.supported_signature_algs().contains(&signature_alg) {
67            return Err(AttestationBuilderError::new(
68                ErrorKind::Request,
69                format!("unsupported signature algorithm: {signature_alg}"),
70            ));
71        }
72
73        if !config.supported_hash_algs().contains(&hash_alg) {
74            return Err(AttestationBuilderError::new(
75                ErrorKind::Request,
76                format!("unsupported hash algorithm: {hash_alg}"),
77            ));
78        }
79
80        if encoding_commitment_root.is_some()
81            && !config
82                .supported_fields()
83                .contains(&FieldKind::EncodingCommitment)
84        {
85            return Err(AttestationBuilderError::new(
86                ErrorKind::Request,
87                "encoding commitment is not supported",
88            ));
89        }
90
91        if let Some(validator) = config.extension_validator() {
92            validator(&extensions)
93                .map_err(|err| AttestationBuilderError::new(ErrorKind::Extension, err))?;
94        }
95
96        Ok(AttestationBuilder {
97            config: self.config,
98            state: Sign {
99                signature_alg,
100                hash_alg,
101                connection_info: None,
102                server_ephemeral_key: None,
103                cert_commitment,
104                encoding_commitment_root,
105                encoder_secret: None,
106                extensions,
107            },
108        })
109    }
110}
111
112impl AttestationBuilder<'_, Sign> {
113    /// Sets the connection information.
114    pub fn connection_info(&mut self, connection_info: ConnectionInfo) -> &mut Self {
115        self.state.connection_info = Some(connection_info);
116        self
117    }
118
119    /// Sets the server ephemeral key.
120    pub fn server_ephemeral_key(&mut self, key: ServerEphemKey) -> &mut Self {
121        self.state.server_ephemeral_key = Some(key);
122        self
123    }
124
125    /// Sets the encoder secret.
126    pub fn encoder_secret(&mut self, secret: EncoderSecret) -> &mut Self {
127        self.state.encoder_secret = Some(secret);
128        self
129    }
130
131    /// Adds an extension to the attestation.
132    pub fn extension(&mut self, extension: Extension) -> &mut Self {
133        self.state.extensions.push(extension);
134        self
135    }
136
137    /// Builds the attestation.
138    pub fn build(self, provider: &CryptoProvider) -> Result<Attestation, AttestationBuilderError> {
139        let Sign {
140            signature_alg,
141            hash_alg,
142            connection_info,
143            server_ephemeral_key,
144            cert_commitment,
145            encoding_commitment_root,
146            encoder_secret,
147            extensions,
148        } = self.state;
149
150        let hasher = provider.hash.get(&hash_alg).map_err(|_| {
151            AttestationBuilderError::new(
152                ErrorKind::Config,
153                format!("accepted hash algorithm {hash_alg} but it's missing in the provider"),
154            )
155        })?;
156        let signer = provider.signer.get(&signature_alg).map_err(|_| {
157            AttestationBuilderError::new(
158                ErrorKind::Config,
159                format!(
160                    "accepted signature algorithm {signature_alg} but it's missing in the provider"
161                ),
162            )
163        })?;
164
165        let encoding_commitment = if let Some(root) = encoding_commitment_root {
166            let Some(secret) = encoder_secret else {
167                return Err(AttestationBuilderError::new(
168                    ErrorKind::Field,
169                    "encoding commitment requested but encoder_secret was not set",
170                ));
171            };
172
173            Some(EncodingCommitment { root, secret })
174        } else {
175            None
176        };
177
178        let mut field_id = FieldId::default();
179
180        let body = Body {
181            verifying_key: field_id.next(signer.verifying_key()),
182            connection_info: field_id.next(connection_info.ok_or_else(|| {
183                AttestationBuilderError::new(ErrorKind::Field, "connection info was not set")
184            })?),
185            server_ephemeral_key: field_id.next(server_ephemeral_key.ok_or_else(|| {
186                AttestationBuilderError::new(ErrorKind::Field, "handshake data was not set")
187            })?),
188            cert_commitment: field_id.next(cert_commitment),
189            encoding_commitment: encoding_commitment.map(|commitment| field_id.next(commitment)),
190            plaintext_hashes: Default::default(),
191            extensions: extensions
192                .into_iter()
193                .map(|extension| field_id.next(extension))
194                .collect(),
195        };
196
197        let header = Header {
198            id: rng().random(),
199            version: VERSION,
200            root: body.root(hasher),
201        };
202
203        let signature = signer
204            .sign(&CanonicalSerialize::serialize(&header))
205            .map_err(|err| AttestationBuilderError::new(ErrorKind::Signature, err))?;
206
207        Ok(Attestation {
208            signature,
209            header,
210            body,
211        })
212    }
213}
214
215/// Error for [`AttestationBuilder`].
216#[derive(Debug, thiserror::Error)]
217pub struct AttestationBuilderError {
218    kind: ErrorKind,
219    source: Option<Box<dyn Error + Send + Sync + 'static>>,
220}
221
222#[derive(Debug)]
223enum ErrorKind {
224    Request,
225    Config,
226    Field,
227    Signature,
228    Extension,
229}
230
231impl AttestationBuilderError {
232    fn new<E>(kind: ErrorKind, error: E) -> Self
233    where
234        E: Into<Box<dyn Error + Send + Sync + 'static>>,
235    {
236        Self {
237            kind,
238            source: Some(error.into()),
239        }
240    }
241
242    /// Returns whether the error originates from a bad request.
243    pub fn is_request(&self) -> bool {
244        matches!(self.kind, ErrorKind::Request)
245    }
246}
247
248impl std::fmt::Display for AttestationBuilderError {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        match self.kind {
251            ErrorKind::Request => f.write_str("request error")?,
252            ErrorKind::Config => f.write_str("config error")?,
253            ErrorKind::Field => f.write_str("field error")?,
254            ErrorKind::Signature => f.write_str("signature error")?,
255            ErrorKind::Extension => f.write_str("extension error")?,
256        }
257
258        if let Some(source) = &self.source {
259            write!(f, " caused by: {}", source)?;
260        }
261
262        Ok(())
263    }
264}
265
266#[cfg(test)]
267mod test {
268    use rstest::{fixture, rstest};
269    use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
270
271    use crate::{
272        connection::{HandshakeData, HandshakeDataV1_2},
273        fixtures::{
274            encoder_secret, encoding_provider, request_fixture, ConnectionFixture, RequestFixture,
275        },
276        hash::Blake3,
277        transcript::Transcript,
278    };
279
280    use super::*;
281
282    #[fixture]
283    #[once]
284    fn attestation_config() -> AttestationConfig {
285        AttestationConfig::builder()
286            .supported_signature_algs([SignatureAlgId::SECP256K1])
287            .build()
288            .unwrap()
289    }
290
291    #[fixture]
292    #[once]
293    fn crypto_provider() -> CryptoProvider {
294        let mut provider = CryptoProvider::default();
295        provider.signer.set_secp256k1(&[42u8; 32]).unwrap();
296        provider
297    }
298
299    #[rstest]
300    fn test_attestation_builder_accept_unsupported_signer() {
301        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
302        let connection = ConnectionFixture::tlsnotary(transcript.length());
303
304        let RequestFixture { request, .. } = request_fixture(
305            transcript,
306            encoding_provider(GET_WITH_HEADER, OK_JSON),
307            connection,
308            Blake3::default(),
309            Vec::new(),
310        );
311
312        let attestation_config = AttestationConfig::builder()
313            .supported_signature_algs([SignatureAlgId::SECP256R1])
314            .build()
315            .unwrap();
316
317        let err = Attestation::builder(&attestation_config)
318            .accept_request(request)
319            .err()
320            .unwrap();
321        assert!(err.is_request());
322    }
323
324    #[rstest]
325    fn test_attestation_builder_accept_unsupported_hasher() {
326        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
327        let connection = ConnectionFixture::tlsnotary(transcript.length());
328
329        let RequestFixture { request, .. } = request_fixture(
330            transcript,
331            encoding_provider(GET_WITH_HEADER, OK_JSON),
332            connection,
333            Blake3::default(),
334            Vec::new(),
335        );
336
337        let attestation_config = AttestationConfig::builder()
338            .supported_signature_algs([SignatureAlgId::SECP256K1])
339            .supported_hash_algs([HashAlgId::KECCAK256])
340            .build()
341            .unwrap();
342
343        let err = Attestation::builder(&attestation_config)
344            .accept_request(request)
345            .err()
346            .unwrap();
347        assert!(err.is_request());
348    }
349
350    #[rstest]
351    fn test_attestation_builder_accept_unsupported_encoding_commitment() {
352        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
353        let connection = ConnectionFixture::tlsnotary(transcript.length());
354
355        let RequestFixture { request, .. } = request_fixture(
356            transcript,
357            encoding_provider(GET_WITH_HEADER, OK_JSON),
358            connection,
359            Blake3::default(),
360            Vec::new(),
361        );
362
363        let attestation_config = AttestationConfig::builder()
364            .supported_signature_algs([SignatureAlgId::SECP256K1])
365            .supported_fields([
366                FieldKind::ConnectionInfo,
367                FieldKind::ServerEphemKey,
368                FieldKind::ServerIdentityCommitment,
369            ])
370            .build()
371            .unwrap();
372
373        let err = Attestation::builder(&attestation_config)
374            .accept_request(request)
375            .err()
376            .unwrap();
377        assert!(err.is_request());
378    }
379
380    #[rstest]
381    fn test_attestation_builder_sign_missing_signer(attestation_config: &AttestationConfig) {
382        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
383        let connection = ConnectionFixture::tlsnotary(transcript.length());
384
385        let RequestFixture { request, .. } = request_fixture(
386            transcript,
387            encoding_provider(GET_WITH_HEADER, OK_JSON),
388            connection,
389            Blake3::default(),
390            Vec::new(),
391        );
392
393        let attestation_builder = Attestation::builder(attestation_config)
394            .accept_request(request)
395            .unwrap();
396
397        let mut provider = CryptoProvider::default();
398        provider.signer.set_secp256r1(&[42u8; 32]).unwrap();
399
400        let err = attestation_builder.build(&provider).err().unwrap();
401        assert!(matches!(err.kind, ErrorKind::Config));
402    }
403
404    #[rstest]
405    fn test_attestation_builder_sign_missing_encoding_seed(
406        attestation_config: &AttestationConfig,
407        crypto_provider: &CryptoProvider,
408    ) {
409        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
410        let connection = ConnectionFixture::tlsnotary(transcript.length());
411
412        let RequestFixture { request, .. } = request_fixture(
413            transcript,
414            encoding_provider(GET_WITH_HEADER, OK_JSON),
415            connection.clone(),
416            Blake3::default(),
417            Vec::new(),
418        );
419
420        let mut attestation_builder = Attestation::builder(attestation_config)
421            .accept_request(request)
422            .unwrap();
423
424        let ConnectionFixture {
425            connection_info,
426            server_cert_data,
427            ..
428        } = connection;
429
430        let HandshakeData::V1_2(HandshakeDataV1_2 {
431            server_ephemeral_key,
432            ..
433        }) = server_cert_data.handshake;
434
435        attestation_builder
436            .connection_info(connection_info)
437            .server_ephemeral_key(server_ephemeral_key);
438
439        let err = attestation_builder.build(crypto_provider).err().unwrap();
440        assert!(matches!(err.kind, ErrorKind::Field));
441    }
442
443    #[rstest]
444    fn test_attestation_builder_sign_missing_server_ephemeral_key(
445        attestation_config: &AttestationConfig,
446        crypto_provider: &CryptoProvider,
447    ) {
448        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
449        let connection = ConnectionFixture::tlsnotary(transcript.length());
450
451        let RequestFixture { request, .. } = request_fixture(
452            transcript,
453            encoding_provider(GET_WITH_HEADER, OK_JSON),
454            connection.clone(),
455            Blake3::default(),
456            Vec::new(),
457        );
458
459        let mut attestation_builder = Attestation::builder(attestation_config)
460            .accept_request(request)
461            .unwrap();
462
463        let ConnectionFixture {
464            connection_info, ..
465        } = connection;
466
467        attestation_builder
468            .connection_info(connection_info)
469            .encoder_secret(encoder_secret());
470
471        let err = attestation_builder.build(crypto_provider).err().unwrap();
472        assert!(matches!(err.kind, ErrorKind::Field));
473    }
474
475    #[rstest]
476    fn test_attestation_builder_sign_missing_connection_info(
477        attestation_config: &AttestationConfig,
478        crypto_provider: &CryptoProvider,
479    ) {
480        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
481        let connection = ConnectionFixture::tlsnotary(transcript.length());
482
483        let RequestFixture { request, .. } = request_fixture(
484            transcript,
485            encoding_provider(GET_WITH_HEADER, OK_JSON),
486            connection.clone(),
487            Blake3::default(),
488            Vec::new(),
489        );
490
491        let mut attestation_builder = Attestation::builder(attestation_config)
492            .accept_request(request)
493            .unwrap();
494
495        let ConnectionFixture {
496            server_cert_data, ..
497        } = connection;
498
499        let HandshakeData::V1_2(HandshakeDataV1_2 {
500            server_ephemeral_key,
501            ..
502        }) = server_cert_data.handshake;
503
504        attestation_builder
505            .server_ephemeral_key(server_ephemeral_key)
506            .encoder_secret(encoder_secret());
507
508        let err = attestation_builder.build(crypto_provider).err().unwrap();
509        assert!(matches!(err.kind, ErrorKind::Field));
510    }
511
512    #[rstest]
513    fn test_attestation_builder_reject_extensions_by_default(
514        attestation_config: &AttestationConfig,
515    ) {
516        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
517        let connection = ConnectionFixture::tlsnotary(transcript.length());
518
519        let RequestFixture { request, .. } = request_fixture(
520            transcript,
521            encoding_provider(GET_WITH_HEADER, OK_JSON),
522            connection.clone(),
523            Blake3::default(),
524            vec![Extension {
525                id: b"foo".to_vec(),
526                value: b"bar".to_vec(),
527            }],
528        );
529
530        let err = Attestation::builder(attestation_config)
531            .accept_request(request)
532            .unwrap_err();
533
534        assert!(matches!(err.kind, ErrorKind::Extension));
535    }
536
537    #[rstest]
538    fn test_attestation_builder_accept_extension(crypto_provider: &CryptoProvider) {
539        let attestation_config = AttestationConfig::builder()
540            .supported_signature_algs([SignatureAlgId::SECP256K1])
541            .extension_validator(|_| Ok(()))
542            .build()
543            .unwrap();
544
545        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
546        let connection = ConnectionFixture::tlsnotary(transcript.length());
547
548        let RequestFixture { request, .. } = request_fixture(
549            transcript,
550            encoding_provider(GET_WITH_HEADER, OK_JSON),
551            connection.clone(),
552            Blake3::default(),
553            vec![Extension {
554                id: b"foo".to_vec(),
555                value: b"bar".to_vec(),
556            }],
557        );
558
559        let mut attestation_builder = Attestation::builder(&attestation_config)
560            .accept_request(request)
561            .unwrap();
562
563        let ConnectionFixture {
564            server_cert_data,
565            connection_info,
566            ..
567        } = connection;
568
569        let HandshakeData::V1_2(HandshakeDataV1_2 {
570            server_ephemeral_key,
571            ..
572        }) = server_cert_data.handshake;
573
574        attestation_builder
575            .connection_info(connection_info)
576            .server_ephemeral_key(server_ephemeral_key)
577            .encoder_secret(encoder_secret());
578
579        let attestation = attestation_builder.build(crypto_provider).unwrap();
580
581        assert_eq!(attestation.body.extensions().count(), 1);
582    }
583}