use std::error::Error;
use rand::{thread_rng, Rng};
use crate::{
attestation::{
Attestation, AttestationConfig, Body, EncodingCommitment, FieldId, FieldKind, Header,
ServerCertCommitment, VERSION,
},
connection::{ConnectionInfo, ServerEphemKey},
hash::{HashAlgId, TypedHash},
request::Request,
serialize::CanonicalSerialize,
signing::SignatureAlgId,
CryptoProvider,
};
pub struct Accept {}
pub struct Sign {
signature_alg: SignatureAlgId,
hash_alg: HashAlgId,
connection_info: Option<ConnectionInfo>,
server_ephemeral_key: Option<ServerEphemKey>,
cert_commitment: ServerCertCommitment,
encoding_commitment_root: Option<TypedHash>,
encoding_seed: Option<Vec<u8>>,
}
pub struct AttestationBuilder<'a, T = Accept> {
config: &'a AttestationConfig,
state: T,
}
impl<'a> AttestationBuilder<'a, Accept> {
pub fn new(config: &'a AttestationConfig) -> Self {
Self {
config,
state: Accept {},
}
}
pub fn accept_request(
self,
request: Request,
) -> Result<AttestationBuilder<'a, Sign>, AttestationBuilderError> {
let config = self.config;
let Request {
signature_alg,
hash_alg,
server_cert_commitment: cert_commitment,
encoding_commitment_root,
} = request;
if !config.supported_signature_algs().contains(&signature_alg) {
return Err(AttestationBuilderError::new(
ErrorKind::Request,
format!("unsupported signature algorithm: {signature_alg}"),
));
}
if !config.supported_hash_algs().contains(&hash_alg) {
return Err(AttestationBuilderError::new(
ErrorKind::Request,
format!("unsupported hash algorithm: {hash_alg}"),
));
}
if encoding_commitment_root.is_some()
&& !config
.supported_fields()
.contains(&FieldKind::EncodingCommitment)
{
return Err(AttestationBuilderError::new(
ErrorKind::Request,
"encoding commitment is not supported",
));
}
Ok(AttestationBuilder {
config: self.config,
state: Sign {
signature_alg,
hash_alg,
connection_info: None,
server_ephemeral_key: None,
cert_commitment,
encoding_commitment_root,
encoding_seed: None,
},
})
}
}
impl AttestationBuilder<'_, Sign> {
pub fn connection_info(&mut self, connection_info: ConnectionInfo) -> &mut Self {
self.state.connection_info = Some(connection_info);
self
}
pub fn server_ephemeral_key(&mut self, key: ServerEphemKey) -> &mut Self {
self.state.server_ephemeral_key = Some(key);
self
}
pub fn encoding_seed(&mut self, seed: Vec<u8>) -> &mut Self {
self.state.encoding_seed = Some(seed);
self
}
pub fn build(self, provider: &CryptoProvider) -> Result<Attestation, AttestationBuilderError> {
let Sign {
signature_alg,
hash_alg,
connection_info,
server_ephemeral_key,
cert_commitment,
encoding_commitment_root,
encoding_seed,
} = self.state;
let hasher = provider.hash.get(&hash_alg).map_err(|_| {
AttestationBuilderError::new(
ErrorKind::Config,
format!("accepted hash algorithm {hash_alg} but it's missing in the provider"),
)
})?;
let signer = provider.signer.get(&signature_alg).map_err(|_| {
AttestationBuilderError::new(
ErrorKind::Config,
format!(
"accepted signature algorithm {signature_alg} but it's missing in the provider"
),
)
})?;
let encoding_commitment = if let Some(root) = encoding_commitment_root {
let Some(seed) = encoding_seed else {
return Err(AttestationBuilderError::new(
ErrorKind::Field,
"encoding commitment requested but seed was not set",
));
};
Some(EncodingCommitment { root, seed })
} else {
None
};
let mut field_id = FieldId::default();
let body = Body {
verifying_key: field_id.next(signer.verifying_key()),
connection_info: field_id.next(connection_info.ok_or_else(|| {
AttestationBuilderError::new(ErrorKind::Field, "connection info was not set")
})?),
server_ephemeral_key: field_id.next(server_ephemeral_key.ok_or_else(|| {
AttestationBuilderError::new(ErrorKind::Field, "handshake data was not set")
})?),
cert_commitment: field_id.next(cert_commitment),
encoding_commitment: encoding_commitment.map(|commitment| field_id.next(commitment)),
plaintext_hashes: Default::default(),
};
let header = Header {
id: thread_rng().gen(),
version: VERSION,
root: body.root(hasher),
};
let signature = signer
.sign(&CanonicalSerialize::serialize(&header))
.map_err(|err| AttestationBuilderError::new(ErrorKind::Signature, err))?;
Ok(Attestation {
signature,
header,
body,
})
}
}
#[derive(Debug, thiserror::Error)]
pub struct AttestationBuilderError {
kind: ErrorKind,
source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
#[derive(Debug)]
enum ErrorKind {
Request,
Config,
Field,
Signature,
}
impl AttestationBuilderError {
fn new<E>(kind: ErrorKind, error: E) -> Self
where
E: Into<Box<dyn Error + Send + Sync + 'static>>,
{
Self {
kind,
source: Some(error.into()),
}
}
pub fn is_request(&self) -> bool {
matches!(self.kind, ErrorKind::Request)
}
}
impl std::fmt::Display for AttestationBuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind {
ErrorKind::Request => f.write_str("request error")?,
ErrorKind::Config => f.write_str("config error")?,
ErrorKind::Field => f.write_str("field error")?,
ErrorKind::Signature => f.write_str("signature error")?,
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use rstest::{fixture, rstest};
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
use crate::{
connection::{HandshakeData, HandshakeDataV1_2},
fixtures::{
encoder_seed, encoding_provider, request_fixture, ConnectionFixture, RequestFixture,
},
hash::Blake3,
transcript::Transcript,
};
use super::*;
#[fixture]
#[once]
fn attestation_config() -> AttestationConfig {
AttestationConfig::builder()
.supported_signature_algs([SignatureAlgId::SECP256K1])
.build()
.unwrap()
}
#[fixture]
#[once]
fn crypto_provider() -> CryptoProvider {
let mut provider = CryptoProvider::default();
provider.signer.set_secp256k1(&[42u8; 32]).unwrap();
provider
}
#[rstest]
fn test_attestation_builder_accept_unsupported_signer() {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { request, .. } = request_fixture(
transcript,
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection,
Blake3::default(),
);
let attestation_config = AttestationConfig::builder()
.supported_signature_algs([SignatureAlgId::SECP256R1])
.build()
.unwrap();
let err = Attestation::builder(&attestation_config)
.accept_request(request)
.err()
.unwrap();
assert!(err.is_request());
}
#[rstest]
fn test_attestation_builder_accept_unsupported_hasher() {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { request, .. } = request_fixture(
transcript,
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection,
Blake3::default(),
);
let attestation_config = AttestationConfig::builder()
.supported_signature_algs([SignatureAlgId::SECP256K1])
.supported_hash_algs([HashAlgId::KECCAK256])
.build()
.unwrap();
let err = Attestation::builder(&attestation_config)
.accept_request(request)
.err()
.unwrap();
assert!(err.is_request());
}
#[rstest]
fn test_attestation_builder_accept_unsupported_encoding_commitment() {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { request, .. } = request_fixture(
transcript,
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection,
Blake3::default(),
);
let attestation_config = AttestationConfig::builder()
.supported_signature_algs([SignatureAlgId::SECP256K1])
.supported_fields([
FieldKind::ConnectionInfo,
FieldKind::ServerEphemKey,
FieldKind::ServerIdentityCommitment,
])
.build()
.unwrap();
let err = Attestation::builder(&attestation_config)
.accept_request(request)
.err()
.unwrap();
assert!(err.is_request());
}
#[rstest]
fn test_attestation_builder_sign_missing_signer(attestation_config: &AttestationConfig) {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { request, .. } = request_fixture(
transcript,
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection,
Blake3::default(),
);
let attestation_builder = Attestation::builder(attestation_config)
.accept_request(request)
.unwrap();
let mut provider = CryptoProvider::default();
provider.signer.set_secp256r1(&[42u8; 32]).unwrap();
let err = attestation_builder.build(&provider).err().unwrap();
assert!(matches!(err.kind, ErrorKind::Config));
}
#[rstest]
fn test_attestation_builder_sign_missing_encoding_seed(
attestation_config: &AttestationConfig,
crypto_provider: &CryptoProvider,
) {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { request, .. } = request_fixture(
transcript,
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection.clone(),
Blake3::default(),
);
let mut attestation_builder = Attestation::builder(attestation_config)
.accept_request(request)
.unwrap();
let ConnectionFixture {
connection_info,
server_cert_data,
..
} = connection;
let HandshakeData::V1_2(HandshakeDataV1_2 {
server_ephemeral_key,
..
}) = server_cert_data.handshake;
attestation_builder
.connection_info(connection_info)
.server_ephemeral_key(server_ephemeral_key);
let err = attestation_builder.build(crypto_provider).err().unwrap();
assert!(matches!(err.kind, ErrorKind::Field));
}
#[rstest]
fn test_attestation_builder_sign_missing_server_ephemeral_key(
attestation_config: &AttestationConfig,
crypto_provider: &CryptoProvider,
) {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { request, .. } = request_fixture(
transcript,
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection.clone(),
Blake3::default(),
);
let mut attestation_builder = Attestation::builder(attestation_config)
.accept_request(request)
.unwrap();
let ConnectionFixture {
connection_info, ..
} = connection;
attestation_builder
.connection_info(connection_info)
.encoding_seed(encoder_seed().to_vec());
let err = attestation_builder.build(crypto_provider).err().unwrap();
assert!(matches!(err.kind, ErrorKind::Field));
}
#[rstest]
fn test_attestation_builder_sign_missing_connection_info(
attestation_config: &AttestationConfig,
crypto_provider: &CryptoProvider,
) {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { request, .. } = request_fixture(
transcript,
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection.clone(),
Blake3::default(),
);
let mut attestation_builder = Attestation::builder(attestation_config)
.accept_request(request)
.unwrap();
let ConnectionFixture {
server_cert_data, ..
} = connection;
let HandshakeData::V1_2(HandshakeDataV1_2 {
server_ephemeral_key,
..
}) = server_cert_data.handshake;
attestation_builder
.server_ephemeral_key(server_ephemeral_key)
.encoding_seed(encoder_seed().to_vec());
let err = attestation_builder.build(crypto_provider).err().unwrap();
assert!(matches!(err.kind, ErrorKind::Field));
}
}