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

> Discover how Iroh handles authentication with raw public key TLS, bypassing traditional certificates. Learn about its RFC 7250 implementation binding Ed25519 identity directly to TLS.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: deep-dive
- Published: 2026-06-18

---

**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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/tls/name.rs) uses BASE32_DNSSEC to ensure the public key is representable as a valid domain label.

```rust
// 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`](https://github.com/n0-computer/iroh/blob/main/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:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs):

```rust
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:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/key.rs) serves as the sole cryptographic root of trust, configured through the `Endpoint` builder in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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.