# ML-DSA Certificates on Cisco Routers – IPsec Series, Part 10

> Source: <https://blogs.cisco.com/developer/ml-dsa-certificates-on-cisco-routers-ipsec-series-part-10>
> Published: 2026-09-08 17:48:10+00:00

Part 9 made the key exchange quantum-safe. ML-KEM-768 is running on all three routers, fragmentation is handling the bigger messages, and the phased rollout lets you migrate at your own pace. But authentication is still a pre-shared key, and at scale that doesn’t work.

This post closes the gap. We swap PSK for **ML-DSA certificates** (FIPS 204), the same signature scheme from Parts 6-8 in the container labs. The difference: on IOS XE, the path from “I have a key” to “I have a certificate” requires some work. And once the tunnel is up, we measure exactly what ML-DSA costs on the wire.

## The PKI lives on your workstation

The entire ML-DSA identity for each router is built on your workstation with OpenSSL 3.5+ and imported as a PKCS#12 bundle (a single password-encrypted file carrying the private key, its certificate, and the issuing CA chain together).

This is the verified path for getting ML-DSA identities onto the routers. We generate a root CA per parameter set, an identity certificate for R1, R2, and R3, and a PKCS#12 bundle for each.

```
=== Artifact sizes in bytes (R1 identity, DER-encoded cert)
ALGORITHM          PUBKEY      PRIVKEY         CERT          P12
rsa-2048              294         1218          920         3698
ecdsa-p256             91          138          528         1827
mldsa44              1334         2626         4118        11504
mldsa65              1974         4098         5647        16032
mldsa87              2614         4962         7605        20816
```

Look at that table: a ML-DSA-65 public key is **1,974 bytes against 294** for RSA-2048 and **against 91** for ECDSA P-256. The certificate carrying it is **5,647 bytes against 920**. That’s not a small change, it’s an order of magnitude, and IKEv2 has to carry it.

## Import the bundle

Copy the PKCS#12 file to the router and import:

``` python
R1(config)# crypto pki import TP-MLDSA65 pkcs12 bootflash:/mldsa65-r1.p12 password cisco123
% Importing pkcs12...
Source filename [mldsa65-r1.p12]?
Reading file from bootflash:/mldsa65-r1.p12
CRYPTO_PKI: Imported PKCS12 file successfully.
```

That `password cisco123` protects the bundle for the few seconds it sits on `bootflash:` and nothing else. The bundle gets deleted right after import. Don’t reuse this pattern for a bundle that lives anywhere longer than one import. Delete it immediately:

```
R1# delete /force bootflash:/mldsa65-r1.p12
```

One more thing: the default `revocation-check crl` on the trustpoint means the router tries to download a CRL (Cert Revocation List) before trusting a peer’s certificate. Lab certificates have no CRL distribution point, so the handshake stalls. Fix it:

```
R1(config)# crypto pki trustpoint TP-MLDSA65
R1(ca-trustpoint)# revocation-check none
```

Confirm the router parsed an ML-DSA certificate:

```
R1# show crypto pki certificates verbose TP-MLDSA65
Certificate
  Status: Available
  Version: 3
  Certificate Usage: Signature
  Issuer:
    cn=PQC-LAB-ROOT-mldsa65
  Subject:
    Name: R1-mldsa65
    IP Address: 10.0.12.1
    cn=R1-mldsa65
  Subject Key Info:
    Public Key Algorithm: ML-DSA
    Public Key Size: (1974 bytes)
  Signature Algorithm: ML-DSA-65
  X509v3 extensions:
    X509v3 Subject Alternative Name:
            IP Address : 10.0.12.1
    Extended Key Usage:
        Server Auth
        Client Auth
```

`Public Key Algorithm: ML-DSA`, 1,974 bytes, matching the container lab OpenSSL output exactly.

Both `Server Auth` and `Client Auth` EKUs are needed. An IKEv2 peer is initiator on one negotiation and responder on the next, so a client-only certificate fails half the time.

## Swap PSK for ML-DSA

Do this on **both R1 and R3**. The key exchange config doesn’t change at all: `pqc mlkem768 optional` from Part 9 stays exactly as it is. Only the authentication method changes.

```
crypto ikev2 profile MLDSA-PROFILE
 match identity remote address 10.0.23.2 255.255.255.255
 identity local address 10.0.12.1
 authentication local mldsa-sig
 authentication remote mldsa-sig
 pki trustpoint TP-MLDSA65
 dpd 30 5 periodic

crypto ipsec profile MLDSA-IPSEC
 set transform-set CLASSICAL-TS
 set ikev2-profile MLDSA-PROFILE

interface Tunnel0
 tunnel protection ipsec profile MLDSA-IPSEC
```

The router warns you:

```
 Warning: MLDSA Auth packets are extremely large. Please enable IKEv2 Fragmentation.
```

Fragmentation was already enabled in Part 9, so we’re covered. Changing tunnel protection shuts the interface, so bring it back:

```
R1(config)# interface Tunnel0
R1(config-if)# no shutdown
```

## The payoff

```
R1# ping 192.168.100.2
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/4 ms

R1# show crypto ikev2 sa detailed
Tunnel-id Local                 Remote                fvrf/ivrf            Status
2         10.0.12.1/500         10.0.23.2/500         none/none            READY
      Encr: AES-CBC, keysize: 256, PRF: SHA512, Hash: SHA512, DH Grp:20, Auth sign: MLDSA, Auth verify: MLDSA
      PQC Key Exchange: ML-KEM-768
      ...
      IETF Std Fragmentation  enabled.
      Quantum-safe Encryption using PQC: ML-KEM-768
      IETF Std Fragmentation MTU in use: 1372 bytes.
```

There it is, on one line: **`Auth sign: MLDSA, Auth verify: MLDSA`** next to **` PQC Key Exchange: ML-KEM-768`**. Both halves of the tunnel are quantum-safe. Key exchange resists harvest-now-decrypt-later. Authentication resists a future forged identity.

## What ML-DSA actually costs on the wire

The warning said the auth packets are “exetremely large”, but **how** large? We set up a packet capture on R2 (the transit hop that sees every IKE packet without being a crypto endpoint) and measured one complete handshake for each signature algorithm.

| Authentication | IKE_AUTH frames | IKE_AUTH bytes | Whole handshake | vs RSA-2048 | 
|---|---|---|---|---|
| RSA-2048 | 3 | 3,462 | 7 frames / 7,111 B | 1.0x | 
| ML-DSA-44 | 12 | 15,384 | 16 frames / 19,033 B | 4.4x | 
| ML-DSA-65 | 16 | 20,752 | 20 frames / 24,401 B | 6.0x | 
| ML-DSA-87 | 20 | 27,848 | 24 frames / 31,497 B | 8.0x | 

`IKE_SA_INIT` is 2 frames and about 1.1 KB in every case: it only carries the classical ECDH exchange (group 20), whose public value is 512 bytes. The ML-KEM `IKE_INTERMEDIATE` exchange is also constant at 2 frames and 2,548 bytes (the ML-KEM-768 encapsulation key alone is 1,184 bytes). Neither changes with the signature algorithm. All the *variable* growth is in `IKE_AUTH`, which is where the certificate and the signature travel.

The largest ML-DSA frame is 1,422 bytes in every run. That’s the fragmentation MTU doing its job: instead of one enormous datagram you get a dozen or more well-behaved ones. Exactly why the router nags you about fragmentation.

**Choosing a parameter set.** ML-DSA-65 costs 35% more IKE_AUTH bytes than ML-DSA-44 and buys you NIST security category 3 instead of 2. ML-DSA-87 costs another 34% on top for category 5. Unless you have a specific mandate, **65 is the sensible default**.

**Is 20 KB a problem?** For a site-to-site tunnel that negotiates once and rekeys every 24 hours, no. For a hub terminating thousands of DMVPN spokes that all reconnect after a power event, definitely something to factor into capacity planning. The cost is per-negotiation, not per-packet: once the SA is up, ESP data plane traffic is unchanged.

## Where we stand now

The tunnel is fully quantum-safe. But we just proved it on a single pair of routers. How do you migrate authentication across a whole network? That’s a different problem from key exchange, and the answer is less comfortable. Key exchange negotiates with `optional`; authentication has no fallback keyword. Part 11 walks through the migration playbook, the experiments that show where it breaks, and the operational gotchas that only show up on real hardware.

See you there. Bring your planning hat.
