Skip to content

feat: Implement TLS Support#15

Open
kyokuping wants to merge 7 commits intomainfrom
feat/tls
Open

feat: Implement TLS Support#15
kyokuping wants to merge 7 commits intomainfrom
feat/tls

Conversation

@kyokuping
Copy link
Copy Markdown
Owner

@kyokuping kyokuping commented Apr 5, 2026

Summary

This PR introduces TLS support to the Damas server, enabling secure HTTPS communication.

Key Changes

  • feature:
    • Added tls block to the configuration config.kdl to specify certificate and private key paths.
    • Implemented custom TLSCertificate and TLSPrivateKey types for robust PEM parsing and validation during configuration loading.
    • Add PerformanceConfig for performance tuning and support ring/aws-lc-rs providers via feature flags.
  • refactor : Introduced PerformanceConfig for modular performance tuning and enabled switchable crypto providers via feature flags
  • test : Added a comprehensive integration test suite in test/tls_test.rs that verifies the TLS handshake and data transmission using an in-memory duplex stream.

Technical Notes

  • Implemented a custom MemoryStream that satisfies compio's AsyncRead/AsyncWrite traits to enable high-speed integration testing without actual network I/O.
  • Used unsafe only for zero-copy memory transfers within the MemoryStream implementation, ensuring compatibility with compio's IoBuf requirements.

Quality Control

  • TLS tests passed using cargo-nextest.
  • Dependency check performed via cargo-shear

Related Issues

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 5, 2026

Codecov Report

❌ Patch coverage is 68.57143% with 154 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/config.rs 70.30% 62 Missing and 6 partials ⚠️
test/tls_test.rs 70.49% 33 Missing and 21 partials ⚠️
src/server.rs 22.58% 22 Missing and 2 partials ⚠️
src/cert.rs 86.20% 2 Missing and 2 partials ⚠️
src/http.rs 55.55% 3 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (68.57%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Flag Coverage Δ
rust 86.55% <68.57%> (-3.92%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/error/error_registry.rs 99.47% <100.00%> (+<0.01%) ⬆️
src/lib.rs 100.00% <ø> (ø)
src/router.rs 96.30% <100.00%> (ø)
test/main_test.rs 90.31% <100.00%> (+1.07%) ⬆️
src/cert.rs 86.20% <86.20%> (ø)
src/http.rs 71.42% <55.55%> (-4.85%) ⬇️
src/server.rs 69.03% <22.58%> (-7.30%) ⬇️
test/tls_test.rs 70.49% <70.49%> (ø)
src/config.rs 79.08% <70.30%> (-9.65%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces TLS support and configurable performance settings, enabling the server to handle encrypted traffic with a choice of crypto providers. Key additions include certificate and private key configuration, a new TlsAcceptor implementation, and integration tests for handshakes. Review feedback identifies critical bugs where non-existent methods are used for PEM loading in src/config.rs, along with suggestions to remove unused imports, fix duplicate toolchain components, and replace debugging println! calls with proper tracing.

Comment thread rust-toolchain.toml Outdated
[toolchain]
channel = "1.90.0"
components = ["rustfmt", "clippy"]
components = ["rustfmt", "clippy", "rustfmt", "rust-src", "rust-analyzer"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The rustfmt component is listed twice in the components array. Please remove the duplicate entry to keep the configuration clean.

Suggested change
components = ["rustfmt", "clippy", "rustfmt", "rust-src", "rust-analyzer"]
components = ["rustfmt", "clippy", "rust-src", "rust-analyzer"]

@kyokuping
Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist
Copy link
Copy Markdown

Warning

Gemini is experiencing higher than usual traffic and was unable to create the review. Please try again in a few hours by commenting /gemini review.

@kyokuping
Copy link
Copy Markdown
Owner Author

/gemini review

Repository owner deleted a comment from gemini-code-assist bot Apr 14, 2026
Repository owner deleted a comment from gemini-code-assist bot Apr 14, 2026
Repository owner deleted a comment from gemini-code-assist bot Apr 14, 2026
Repository owner deleted a comment from gemini-code-assist bot Apr 14, 2026
Repository owner deleted a comment from gemini-code-assist bot Apr 14, 2026
Repository owner deleted a comment from gemini-code-assist bot Apr 14, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces TLS support to the server by integrating rustls and compio-tls, adding configuration options for certificates, private keys, and crypto providers. It also refactors performance settings into a dedicated configuration block. Review feedback highlights critical bugs in the TLS configuration decoding logic, specifically regarding certificate chain support and type mismatches during PEM parsing. An improvement was also suggested for the asynchronous write logic in the test suite's memory stream implementation.

Comment thread src/config.rs
Comment on lines +75 to 180
impl<S: ErrorSpan> knus::DecodeScalar<S> for TLSCertificate {
fn raw_decode(
val: &Spanned<Literal, S>,
_: &mut knus::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
match &**val {
Literal::String(s) => {
let path = Path::new(&**s);
check_path_safety(path, "cert").map_err(|e| DecodeError::Conversion {
span: val.span().clone(),
source: e.into(),
})?;
CertificateDer::from_pem_file(path)
.map(TLSCertificate)
.map_err(|e| DecodeError::Conversion {
span: val.span().clone(),
source: Box::new(e),
})
}
_ => Err(DecodeError::scalar_kind(Kind::String, val)),
}
}
fn type_check(type_name: &Option<Spanned<TypeName, S>>, ctx: &mut knus::decode::Context<S>) {
if let Some(typ) = type_name {
ctx.emit_error(DecodeError::TypeName {
span: typ.span().clone(),
found: Some((**typ).clone()),
expected: ExpectedType::no_type(),
rust_type: "TLSCertificate",
});
}
}
}

#[derive(Debug, PartialEq)]
pub struct TLSPrivateKey(pub PrivateKeyDer<'static>);

impl TryFrom<PathBuf> for TLSPrivateKey {
type Error = anyhow::Error;

fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
let data =
std::fs::read(&path).with_context(|| format!("couldn't find key file: {:?}", path))?;

let mut reader = &data[..];
let key = rustls_pemfile::private_key(&mut reader)
.map_err(|e| anyhow!("PEM parsing failed: {}", e))?
.ok_or_else(|| anyhow!("key file is empty: {:?}", path))?;

Ok(TLSPrivateKey(key))
}
}

impl FromStr for TLSPrivateKey {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(PathBuf::from(s))
}
}

impl From<TLSPrivateKey> for PrivateKeyDer<'static> {
fn from(value: TLSPrivateKey) -> Self {
value.0
}
}

impl Clone for TLSPrivateKey {
fn clone(&self) -> Self {
TLSPrivateKey(self.0.clone_key())
}
}

impl<S: ErrorSpan> knus::DecodeScalar<S> for TLSPrivateKey {
fn raw_decode(
val: &Spanned<Literal, S>,
_: &mut knus::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
match &**val {
Literal::String(s) => {
let path = Path::new(&**s);
check_path_safety(path, "key").map_err(|e| DecodeError::Conversion {
span: val.span().clone(),
source: e.into(),
})?;
PrivateKeyDer::from_pem_file(path)
.map(TLSPrivateKey)
.map_err(|e| DecodeError::Conversion {
span: val.span().clone(),
source: e.into(),
})
}
_ => Err(DecodeError::scalar_kind(Kind::String, val)),
}
}

fn type_check(type_name: &Option<Spanned<TypeName, S>>, ctx: &mut knus::decode::Context<S>) {
if let Some(typ) = type_name {
ctx.emit_error(DecodeError::TypeName {
span: typ.span().clone(),
found: Some((**typ).clone()),
expected: ExpectedType::no_type(),
rust_type: "TLSPrivateKey",
});
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The knus::DecodeScalar implementations for both TLSCertificate and TLSPrivateKey have issues that will cause compilation errors and prevent correct handling of TLS configurations.

1. TLSCertificate:
The TLSCertificate struct should hold a Vec<CertificateDer<'static>> to support certificate chains, but it's defined to hold only one. The raw_decode function attempts to use CertificateDer::from_pem_file, which returns a Vec, and then incorrectly tries to construct a TLSCertificate with it.

2. TLSPrivateKey:
Similarly, raw_decode for TLSPrivateKey uses PrivateKeyDer::from_pem_file, which also returns a Vec, and this is incompatible with the TLSPrivateKey struct that expects a single key.

Suggested Fixes:

  1. Update TLSCertificate to hold a Vec:

    #[derive(Clone, Debug, PartialEq)]
    pub struct TLSCertificate(pub Vec<CertificateDer<'static>>);

    And update its From and TryFrom implementations.

  2. Correct the raw_decode implementations. Here's an example:

    For TLSCertificate:

    fn raw_decode(...) -> Result<Self, DecodeError<S>> {
        // ...
        let certs = CertificateDer::from_pem_file(path).map_err(|e| ...)?;
        if certs.is_empty() {
            return Err(DecodeError::Conversion { ... });
        }
        Ok(TLSCertificate(certs))
    }

    For TLSPrivateKey:

    fn raw_decode(...) -> Result<Self, DecodeError<S>> {
        // ...
        let mut keys = PrivateKeyDer::from_pem_file(path).map_err(|e| ...)?;
        if keys.is_empty() {
            return Err(DecodeError::Conversion { ... });
        }
        Ok(TLSPrivateKey(keys.remove(0)))
    }
  3. In src/cert.rs, change the call to with_single_cert to pass the whole vector:

    .with_single_cert(cert.into(), key.into())

These changes are critical for the TLS feature to compile and function correctly.

Comment thread test/tls_test.rs
Comment on lines +67 to +91
if let Err(e) = self.tx.try_send(data) {
tracing::warn!(target: "MemoryStream", "broken pipe");
if e.is_full() {
tracing::debug!(target: "MemoryStream", "channel full, awaiting capacity");
if self.tx.send(e.into_inner()).await.is_err() {
tracing::error!(target: "MemoryStream", "failed to send data: channel closed");
return BufResult(
Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"closed",
)),
buf,
);
}
} else {
tracing::error!(target: "MemoryStream", "write failed: broken pipe");
return BufResult(
Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"closed",
)),
buf,
);
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The write implementation for MemoryStream can be simplified. Instead of using try_send and handling the is_full case manually, you can directly use self.tx.send(data).await. This is cleaner and more idiomatic for async code.

        if self.tx.send(data).await.is_err() {
            tracing::error!(target: "MemoryStream", "write failed: broken pipe");
            return BufResult(
                Err(std::io::Error::new(
                    std::io::ErrorKind::BrokenPipe,
                    "closed",
                )),
                buf,
            );
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant