// Copyright 2023-2025 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: EUPL-1.2

//! Helpers used for advanced TLS configuration.
use std::{path::Path, sync::Arc};

use anyhow::{Context, bail};
use pem::PemObject;
use rustls::{
    CertificateError, OtherError, RootCertStore,
    client::{
        WebPkiServerVerifier,
        danger::{ServerCertVerified, ServerCertVerifier},
    },
    pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime, pem},
};
use sha2::{Digest, Sha256};

/// Verifies that the fingerprint of a certificate matches.
#[derive(Debug)]
pub(crate) struct FingerprintVerifier {
    fingerprint: Vec<u8>,
}

impl FingerprintVerifier {
    // Create a new verifier from a hexadecimal fingerprint representation.
    pub(crate) fn new(hex_fingerprint: &str) -> anyhow::Result<Self> {
        let fingerprint = hex::decode(hex_fingerprint.replace(':', ""))?;
        Ok(FingerprintVerifier { fingerprint })
    }
}

impl ServerCertVerifier for FingerprintVerifier {
    fn verify_server_cert(
        &self,
        end_entity: &CertificateDer,
        _intermediates: &[CertificateDer],
        _server_name: &ServerName,
        _ocsp_response: &[u8],
        _now: UnixTime,
    ) -> Result<ServerCertVerified, rustls::Error> {
        let fingerprint = Sha256::digest(end_entity).to_vec();

        if self.fingerprint == fingerprint {
            Ok(ServerCertVerified::assertion())
        } else {
            Err(rustls::Error::InvalidCertificate(CertificateError::Other(
                OtherError(Arc::from(FingerprintError)),
            )))
        }
    }

    fn verify_tls12_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls12_signature(
            message,
            cert,
            dss,
            &rustls::crypto::ring::default_provider().signature_verification_algorithms,
        )
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls13_signature(
            message,
            cert,
            dss,
            &rustls::crypto::ring::default_provider().signature_verification_algorithms,
        )
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        rustls::crypto::ring::default_provider()
            .signature_verification_algorithms
            .supported_schemes()
    }
}

#[derive(Debug)]
struct FingerprintError;

impl std::fmt::Display for FingerprintError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("certificate fingerprint does not match expectation")
    }
}

impl std::error::Error for FingerprintError {}

/// Verifies the fingerprint and CA for a certificate.
#[derive(Debug)]
pub(crate) struct FingerprintAndWebPkiVerifier(FingerprintVerifier, Arc<WebPkiServerVerifier>);

impl FingerprintAndWebPkiVerifier {
    pub(crate) fn new(
        fingerprint_verifier: FingerprintVerifier,
        roots: impl Into<Arc<RootCertStore>>,
    ) -> anyhow::Result<Self> {
        Ok(Self(
            fingerprint_verifier,
            WebPkiServerVerifier::builder(roots.into()).build()?,
        ))
    }
}

impl ServerCertVerifier for FingerprintAndWebPkiVerifier {
    fn verify_server_cert(
        &self,
        end_entity: &CertificateDer,
        intermediates: &[CertificateDer],
        server_name: &ServerName,
        ocsp_response: &[u8],
        now: UnixTime,
    ) -> Result<ServerCertVerified, rustls::Error> {
        self.0
            .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)?;
        self.1
            .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
    }
    fn verify_tls12_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls12_signature(
            message,
            cert,
            dss,
            &rustls::crypto::ring::default_provider().signature_verification_algorithms,
        )
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls13_signature(
            message,
            cert,
            dss,
            &rustls::crypto::ring::default_provider().signature_verification_algorithms,
        )
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        rustls::crypto::ring::default_provider()
            .signature_verification_algorithms
            .supported_schemes()
    }
}

/// Load certificates and a key file from a pem-encoded file.
#[allow(clippy::collapsible_match)] // False positive: https://github.com/rust-lang/rust-clippy/issues/16933
pub(crate) fn cert_and_key_from_pemfile(
    path: &Path,
) -> anyhow::Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
    let mut certs = Vec::new();
    let mut raw_key: Option<PrivateKeyDer<'static>> = None;

    for item in <(pem::SectionKind, Vec<u8>) as PemObject>::pem_file_iter(path)? {
        let (kind, der) = item?;
        match kind {
            pem::SectionKind::Certificate => {
                certs.push(CertificateDer::from(der));
            }
            pem::SectionKind::EcPrivateKey => {
                if raw_key.replace(PrivateKeyDer::Sec1(der.into())).is_some() {
                    bail!("multiple keys found in {}", path.to_string_lossy());
                }
            }
            pem::SectionKind::PrivateKey => {
                if raw_key.replace(PrivateKeyDer::Pkcs8(der.into())).is_some() {
                    bail!("multiple keys found in {}", path.to_string_lossy());
                }
            }
            pem::SectionKind::RsaPrivateKey => {
                if raw_key.replace(PrivateKeyDer::Pkcs1(der.into())).is_some() {
                    bail!("multiple keys found in {}", path.to_string_lossy());
                }
            }
            _ => {}
        }
    }

    let key = raw_key.with_context(|| format!("no key found in {}", path.to_string_lossy()))?;
    Ok((certs, key))
}

#[cfg(test)]
mod test {
    use super::FingerprintVerifier;

    #[test]
    fn test_fingerprint_without_colons() {
        let fp = "F5F9CFA172B333BB10A5D1C9C51B3F870DB1F1FC58C54B902C6B5A161B0FBF4D";
        assert!(FingerprintVerifier::new(fp).is_ok());
    }

    #[test]
    fn test_fingerprint_with_colons() {
        let fp = "F5:F9:CF:A1:72:B3:33:BB:10:A5:D1:C9:C5:1B:3F:87:0D:B1:F1:FC:58:C5:4B:90:2C:6B:5A:16:1B:0F:BF:4D";
        assert!(FingerprintVerifier::new(fp).is_ok());
    }

    #[test]
    fn test_fingerprint_both_formats_produce_same_result() {
        let with_colons = "F5:F9:CF:A1:72:B3:33:BB:10:A5:D1:C9:C5:1B:3F:87:0D:B1:F1:FC:58:C5:4B:90:2C:6B:5A:16:1B:0F:BF:4D";
        let without_colons = "F5F9CFA172B333BB10A5D1C9C51B3F870DB1F1FC58C54B902C6B5A161B0FBF4D";

        let verifier1 = FingerprintVerifier::new(with_colons).unwrap();
        let verifier2 = FingerprintVerifier::new(without_colons).unwrap();

        assert_eq!(verifier1.fingerprint, verifier2.fingerprint);
    }
}
