How Iroh Handles Authentication Without Traditional Certificates: Raw Public Key TLS

Iroh implements mutual authentication using TLS 1.3 with RFC 7250 raw public keys instead of X.509 certificates, binding each peer's Ed25519 identity directly to the TLS handshake.

Iroh, the open-source peer-to-peer networking toolkit from n0-computer, eliminates the complexity of certificate authorities by authenticating peers through cryptographic raw public keys. This article examines how Iroh handles authentication without traditional certificates by leveraging TLS 1.3 extensions and custom verification logic to establish secure, decentralized connections.

The Raw Public Key Foundation

Instead of relying on X.509 certificate chains, Iroh builds its identity system on TLS 1.3 and the RFC 7250 "Raw Public Key" extension. Every Iroh endpoint generates a permanent Ed25519 key pair defined in iroh-base/src/key.rs.

The private component is a SecretKey, while the public component (EndpointId) serves as the node's permanent identifier. When TlsConfig::new initializes in iroh/src/tls.rs, it creates a ResolveRawPublicKeyCert that satisfies TLS certificate requests by returning a raw public-key structure derived directly from the endpoint's SecretKey. This eliminates the need for a Certificate Authority (CA) or intermediate certificates.

Encoding Identity into TLS Server Names

The BASE32_DNSSEC Encoding Scheme

TLS requires a server name during the handshake, so Iroh encodes the 32-byte EndpointId into a DNS-compatible string. The encode function in iroh/src/tls/name.rs uses BASE32_DNSSEC to ensure the public key is representable as a valid domain label.

// iroh/src/tls/name.rs
pub(crate) fn encode(endpoint_id: EndpointId) -> String {
    format!("{}.iroh.invalid", BASE32_DNSSEC.encode(endpoint_id.as_bytes()))
}

This generates strings like 7dl2ff6emqi2qol3l382krodedij45bn3nh479hqo14a32qpr8kg.iroh.invalid, which are unique to each peer and never collide with real DNS names.

The .iroh.invalid TLD

The .iroh.invalid suffix ensures these names resolve only within the Iroh networking context. During the handshake, the client sends this encoded server name, allowing the verifier to extract the expected EndpointId and compare it against the presented public key.

Custom Certificate Verification in Rust

Iroh replaces the default rustls verification logic with custom implementations in iroh/src/tls/verifier.rs. These verifiers perform byte-level comparison of public keys rather than validating certificate chains.

ServerCertificateVerifier

The ServerCertificateVerifier::verify_server_cert method extracts the endpoint ID from the server name, converts the presented raw public key to a SubjectPublic Key Info (SPKI) structure, and verifies an exact match:

use rustls::client::danger::ServerCertVerified;
use rustls::{Certificate, ServerName};

fn verify_server_cert(
    end_entity: &Certificate,
    server_name: &ServerName,
) -> Result<ServerCertVerified, rustls::Error> {
    // Decode the expected endpoint ID from the server name
    let expected_id = name::decode(server_name.as_str())
        .ok_or(rustls::Error::InvalidCertificate(CertificateError::NotValidForName))?;

    // Convert the presented raw public key to an SPKI structure
    let presented_spki = SubjectPublicKeyInfoDer::from(end_entity.as_ref());

    // Build the expected SPKI from the known endpoint ID
    let expected_spki = rustls::sign::public_key_to_spki(
        &webpki_types::alg_id::ED25519,
        expected_id.as_bytes(),
    );

    if presented_spki != expected_spki {
        return Err(rustls::Error::InvalidCertificate(CertificateError::UnknownIssuer));
    }
    Ok(ServerCertVerified::assertion())
}

ClientCertificateVerifier

The ClientCertificateVerifier::verify_client_cert performs the inverse operation. It requires a raw public key from the client, confirms no intermediate certificates exist, and trusts the signature after verifying the key matches the expected identity.

The Authentication Flow Step-by-Step

The mutual authentication process follows these deterministic steps:

  1. Client initiation: The client initiates a TLS 1.3 handshake, sending its raw public key derived from its SecretKey and the encoded server name of the target endpoint.
  2. Server validation: The server runs ServerCertificateVerifier::verify_server_cert to check that the presented key matches the endpoint ID encoded in the server name.
  3. Server response: The server presents its own raw public key in the TLS certificate slot.
  4. Client validation: The client runs ClientCertificateVerifier::verify_client_cert to verify the server's public key matches the expected EndpointId.

Because verification relies solely on public-key byte comparison, the process is fast, deterministic, and requires no external network queries to validate certificate chains.

Configuring Endpoint Identity in Practice

To create an endpoint with a specific identity, configure the SecretKey through the builder API exposed in iroh/src/endpoint.rs:

use iroh::Endpoint;
use iroh_base::SecretKey;

// Create a secret key from a fixed seed (normally you would generate a random one)
let secret = SecretKey::from_bytes(&[0u8; 32]);
let endpoint = Endpoint::builder()
    .secret_key(secret)          // <-- sets the identity used for authentication
    .bind(([127, 0, 0, 1], 0))?; // bind to a local UDP port

To inspect the server name that will be transmitted during the handshake:

use iroh_base::EndpointId;
use iroh::tls::name::encode;

// Assume we already have an EndpointId (public key)
let endpoint_id: EndpointId = secret.public();
let server_name = encode(endpoint_id);
println!("TLS server name: {}", server_name);
// → e.g. "7dl2ff6emqi2qol3l382krodedij45bn3nh479hqo14a32qpr8kg.iroh.invalid"

Summary

  • Iroh uses raw public keys instead of X.509 certificates, implementing RFC 7250 on top of TLS 1.3 via the ResolveRawPublicKeyCert resolver in iroh/src/tls.rs.
  • Identity is encoded as a BASE32_DNSSEC string in the .iroh.invalid domain, allowing the EndpointId to travel through the TLS server name field.
  • Custom verifiers in iroh/src/tls/verifier.rs perform direct byte comparison of Ed25519 public keys, eliminating CA dependencies while maintaining mutual authentication.
  • The SecretKey in iroh-base/src/key.rs serves as the sole cryptographic root of trust, configured through the Endpoint builder in iroh/src/endpoint.rs.

Frequently Asked Questions

What is RFC 7250 and why does Iroh use it?

RFC 7250 defines the "Raw Public Key" extension for TLS, allowing endpoints to send public keys directly instead of X.509 certificate structures. Iroh uses this to avoid the complexity, size, and trust assumptions of certificate chains, enabling true decentralized authentication where the public key itself is the identity.

How does Iroh prevent server name collisions with real DNS?

Iroh encodes the 32-byte EndpointId using BASE32_DNSSEC and appends the .iroh.invalid top-level domain. This domain is reserved for internal use and will never resolve in the global DNS system, ensuring that Iroh's peer identifiers cannot conflict with legitimate internet hostnames.

Can Iroh interoperate with standard TLS clients that expect X.509 certificates?

No. Iroh's authentication is intentionally incompatible with standard X.509 validation. Both peers must implement the raw public key extension and use the custom verifiers found in iroh/src/tls/verifier.rs. This design trade-off prioritizes simplicity and decentralization over interoperability with legacy PKI systems.

Where is the SecretKey stored and how is it protected?

The SecretKey is an Ed25519 private key defined in iroh-base/src/key.rs. Iroh stores this key in memory during runtime and uses it to sign TLS handshake messages. For persistence, applications must securely store the 32-byte seed used to generate the key. The library provides SecretKey::from_bytes and SecretKey::to_bytes for serialization, leaving the specific storage mechanism (OS keyring, encrypted file, etc.) to the application developer.

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 →