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#[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#[derive(Debug)]
37pub struct AttestationBuilder<'a, T = Accept> {
38 config: &'a AttestationConfig,
39 state: T,
40}
41
42impl<'a> AttestationBuilder<'a, Accept> {
43 pub fn new(config: &'a AttestationConfig) -> Self {
45 Self {
46 config,
47 state: Accept {},
48 }
49 }
50
51 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 pub fn connection_info(&mut self, connection_info: ConnectionInfo) -> &mut Self {
115 self.state.connection_info = Some(connection_info);
116 self
117 }
118
119 pub fn server_ephemeral_key(&mut self, key: ServerEphemKey) -> &mut Self {
121 self.state.server_ephemeral_key = Some(key);
122 self
123 }
124
125 pub fn encoder_secret(&mut self, secret: EncoderSecret) -> &mut Self {
127 self.state.encoder_secret = Some(secret);
128 self
129 }
130
131 pub fn extension(&mut self, extension: Extension) -> &mut Self {
133 self.state.extensions.push(extension);
134 self
135 }
136
137 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#[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 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}