Integrating iroh with Custom TLS Certificate Authorities: Implementation Guide

iroh uses raw public keys (RFC 7250) for end‑to‑end authentication, but you can configure custom TLS certificate authorities for the relay component using CaRootsConfig and CaTlsConfig from iroh-relay/src/tls.rs to validate traditional X.509 certificates while preserving raw‑key verification for node authentication.

The n0-computer/iroh library implements a hybrid TLS architecture that combines raw public key authentication with optional CA‑based TLS for infrastructure components. Understanding how to integrate custom TLS certificate authorities into this stack requires knowledge of where iroh uses traditional certificate validation versus its native raw‑key verification.

Understanding iroh's Hybrid TLS Architecture

iroh splits TLS responsibilities across two layers: the core protocol uses raw public keys for direct peer authentication, while the relay infrastructure can optionally use conventional CA‑based TLS for transport security.

Core Protocol: Raw Public Key Verification (RFC 7250)

In iroh/src/tls.rs, the TlsConfig struct creates a shared configuration that holds the secret key, certificate resolver, and custom verifier. This module builds both client and server QUIC configs using rustls, but with a critical distinction: it implements raw public key authentication instead of X.509 certificate chains.

The verification logic lives in iroh/src/tls/verifier.rs, which implements rustls::client::danger::ServerCertVerifier and rustls::server::danger::ClientCertVerifier. These verifiers check that the peer’s certificate encodes the expected Ed25519 public key (the peer’s iroh ID). The ServerCertificateVerifier decodes the DNS name (the peer’s iroh identifier) and validates that the presented SPKI matches the expected public key, guaranteeing end‑to‑end authentication without a CA.

The resolver in iroh/src/tls/resolver.rs supplies a raw‑public‑key "certificate" via ResolveRawPublicKeyCert, converting an iroh_base::SecretKey into a rustls::sign::CertifiedKey.

Infrastructure Layer: CA‑Based TLS for Relays

While the default iroh transport does not use a traditional CA hierarchy, the relay component (iroh-relay) can be configured with CA‑based TLS. The relay re‑exports two helper types from iroh_relay::tls:

  • CaRootsConfig – holds a set of trusted root certificates as a wrapper around rustls::RootCertStore.
  • CaTlsConfig – combines CaRootsConfig with the usual iroh TLS settings to build rustls::ClientConfig and ServerConfig.

These types are defined in iroh-relay/src/tls.rs. When you need to trust a private CA, you populate a CaRootsConfig with the CA’s PEM‑encoded certificates and pass the resulting CaTlsConfig to the relay. The rest of the iroh stack (raw‑key verification) remains unchanged; the custom CA only influences the TLS handshake performed by the relay.

Implementing Custom CA Support

To integrate custom TLS certificate authorities, you configure the relay infrastructure while leaving the core iroh endpoint configuration unchanged.

Step 1: Create a Custom Root Store

Load your private CA certificates into a CaRootsConfig using rustls’s RootCertStore:

use iroh_relay::tls::{CaRootsConfig, CaTlsConfig};
use rustls::RootCertStore;
use std::{fs, sync::Arc};

// Load PEM‑encoded CA certificates
let ca_pem = fs::read_to_string("my_ca.pem")?;
let mut roots = RootCertStore::empty();
roots.add_parsable_certificates(&rustls_pemfile::certs(&mut ca_pem.as_bytes()))?;

// Build the CA config
let ca_roots = CaRootsConfig::new(Arc::new(roots));

The CaRootsConfig wraps the rustls::RootCertStore and will be used to validate certificates presented by the relay or when the relay acts as a client to other TLS endpoints.

Step 2: Configure the Relay Server

Build a CaTlsConfig and pass it to the relay builder:

use iroh_relay::RelayBuilder;

let ca_tls = CaTlsConfig::builder()
    .with_ca_roots(ca_roots)
    .with_keylog(true)          // optional: enable SSLKEYLOGFILE debugging
    .with_crypto_provider(rustls::crypto::CryptoProvider::default())
    .build()?;

let relay = RelayBuilder::new()
    .tls_config(ca_tls)
    .bind("0.0.0.0:443")?
    .run()
    .await?;

The relay now validates TLS connections against your private CA before passing the raw‑key‑verified stream to the iroh core.

Step 3: Connect Clients Through the Protected Relay

Your iroh endpoint continues to use raw‑key authentication via TlsConfig::make_client_config() and make_server_config(), but connects through the CA‑protected relay:

use iroh::{Endpoint, tls::TlsConfig};
use iroh_base::SecretKey;
use std::sync::Arc;

let secret_key = SecretKey::generate();
let tls_cfg = TlsConfig::new(
    secret_key, 
    8, 
    Arc::new(rustls::crypto::CryptoProvider::default())
);

let endpoint = Endpoint::builder()
    .tls_config(tls_cfg)
    .relay("tls://my-relay.example.com")   // TLS scheme uses the relay's TLS endpoint
    .bind()?
    .run()
    .await?;

The client still authenticates the remote peer with raw public keys (handled by verifier.rs), while the underlying TLS tunnel is validated against the private CA you supplied to the relay.

Complete Working Example

This example demonstrates both the relay configuration with custom CA support and the client connection:

// --- relay/main.rs ---------------------------------------------------------
use iroh_relay::{RelayBuilder, tls::{CaRootsConfig, CaTlsConfig}};
use rustls::{RootCertStore, crypto::CryptoProvider};
use std::{fs, sync::Arc};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Load custom CA
    let ca_pem = fs::read_to_string("my_ca.pem")?;
    let mut roots = RootCertStore::empty();
    roots.add_parsable_certificates(&rustls_pemfile::certs(&mut ca_pem.as_bytes()))?;
    let ca_cfg = CaRootsConfig::new(Arc::new(roots));

    // Build TLS config for the relay
    let tls = CaTlsConfig::builder()
        .with_ca_roots(ca_cfg)
        .with_crypto_provider(CryptoProvider::default())
        .build()?;

    // Run the relay with custom CA validation
    RelayBuilder::new()
        .tls_config(tls)
        .bind("0.0.0.0:443")?
        .run()
        .await?;
    Ok(())
}

// --- client/main.rs --------------------------------------------------------
use iroh::{Endpoint, tls::TlsConfig};
use iroh_base::SecretKey;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Generate iroh node key (raw public key identity)
    let secret = SecretKey::generate();

    // Create TLS config with raw-key verification
    let tls_cfg = TlsConfig::new(
        secret.clone(), 
        8, 
        Arc::new(rustls::crypto::CryptoProvider::default())
    );

    // Connect via the custom-CA-protected relay
    let ep = Endpoint::builder()
        .tls_config(tls_cfg)
        .relay("tls://my-relay.example.com")
        .bind()?
        .run()
        .await?;

    // Use ep normally - all peer authentication uses raw public keys
    Ok(())
}

This architecture ensures that the relay validates the TLS layer with your private CA while the iroh core maintains its end‑to‑end security through raw‑public‑key verification.

Summary

  • Raw public key verification is implemented in iroh/src/tls/verifier.rs and iroh/src/tls/resolver.rs, using ServerCertVerifier to check Ed25519 public keys against iroh node IDs.
  • Custom CA integration happens at the relay layer via iroh-relay/src/tls.rs, specifically through CaRootsConfig (wrapping rustls::RootCertStore) and CaTlsConfig.
  • TLS 1.3 only – Both client and server configurations force TLS 1.3 (PROTOCOL_VERSIONS), with TLS 1.2 explicitly rejected (verify_tls12_signature returns Tls12NotOffered).
  • Zero‑RTT support – TlsConfig creates a ClientSessionMemoryCache (DEFAULT_MAX_TLS_TICKETS) and enables early data (enable_early_data = true), which custom CA configuration does not interfere with.
  • Separation of concerns – The relay validates CA‑based TLS for transport security, while iroh endpoints validate raw keys for application‑layer authentication.

Frequently Asked Questions

Does iroh require a custom CA for peer‑to‑peer connections?

No. Peer‑to‑peer connections in iroh use raw public keys (RFC 7250) for authentication, encoded via ResolveRawPublicKeyCert in iroh/src/tls/resolver.rs. No certificate authority is involved in validating the identity of remote nodes; verification happens by comparing the presented Ed25519 public key against the expected iroh node ID in verifier.rs.

Can I use traditional X.509 certificates instead of raw public keys for iroh nodes?

The default iroh transport in iroh/src/tls.rs is designed specifically for raw public key authentication. While the underlying rustls library supports X.509, iroh’s TlsConfig and verifier.rs implement custom verification logic that expects raw public keys. To use traditional X.509 certificates, you would need to implement custom verifier traits outside the standard iroh TLS configuration.

How does custom CA configuration affect connection performance?

Custom CA configuration does not impact the Zero‑RTT (0‑RTT) support or connection resumption mechanisms. The TlsConfig maintains a ClientSessionMemoryCache with DEFAULT_MAX_TLS_TICKETS and keeps enable_early_data set to true. The custom CA only affects the initial trust anchor validation during the TLS handshake performed by the relay, not the cryptographic performance of the connection.

What security properties does the raw‑public‑key verifier enforce?

The ServerCertVerifier implementation in iroh/src/tls/verifier.rs enforces that the peer’s certificate contains an Ed25519 public key matching the expected iroh ID. It rejects any certificate that does not encode the specific raw public key, effectively preventing man‑in‑the‑middle attacks without requiring a CA hierarchy. This provides end‑to‑end authentication independent of the relay’s TLS configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →