I run my IRC from a VPS. Libera, the new Freenode network, got some spam and, I presume, added IPs from some well known VPS hosts to a sort of soft ban list. Mine included. To get connected, I needed to first identify via SASL. Having always gone the connect → Nickserv route, this caught me by surprise.

This post is twofold. On the one hand I solve the problem for my Weechat setup. On the other hand, I do so for a bot of mine written in Rust.

Weechat

This was actually pretty well documented (see Libera’s Certfp guide and their Weechat guide). I just want future me to have a reference of what exactly I did.

  1. Generate your own certificate

    openssl req -x509 -new -newkey rsa:4096 -sha256 -days 1096 -nodes -out libera.pem -keyout libera.pem
    
  2. Create a certs folder in your Weechat folder. Move the libera.pem file in it.
  3. Configure Weechat to load your cert, Weechat substitutes the %h for its configuration folder.

    /set irc.server.libera.addresses irc.libera.chat/6697
    /set irc.server.libera.ssl on
    /set irc.server.libera.ssl_verify on
    /set irc.server.libera.ssl_cert %h/certs/libera.pem
    
  4. Set up username-password SASL login. This one is needed if your IP is suddenly forced to use SASL to connect to the network and you are locked out. Luckily I already had a nickserv account, otherwise I would have had to first connect from elsewhere.

    /set irc.server.libera.sasl_mechanism PLAIN
    /set irc.server.libera.sasl_username YOURLOGINNICK
    /set irc.server.libera.sasl_password YOURPASSWORD
    
  5. Connect to the network, you should get logged in.
  6. You can /whois yourself to see the cert fingerprint.
  7. Add the cert to your account

    /msg NickServ CERT ADD
    
  8. Now change Weechat setup to use your cert for authentication. You are all set.

    /set irc.server.libera.sasl_mechanism external
    

Rust’s IRC crate

I use the irc crate to run a bot written in Rust. It is nothing special, but has some useful commands. Getting SASL working was a bit more annoying. First you need to make sure you are using version 0.15 or newer. Earlier ones will lead to a ping timeout bug if you try to add SASL the way I will describe here.

First we have to generate a self signed certificate. Sadly this is a bit clumsier than the way I described for Weechat. This has to do with the IRC implementation only supporting a specific type of certificate (at the time of writing), one that is also protected with a password.

  1. Generate self signed certificate and a private key.

    openssl req -x509 -newkey rsa:4096 -keyout myPrivateKey.pem -out myCertificate.crt -days 3650 -nodes
    
  2. Combine the two into a p12 file, this will ask you to choose a password.

    openssl pkcs12 -export -out keyStore.p12 -inkey myPrivateKey.pem -in myCertificate.crt
    
  3. In the bot configuration file (or however you configure your IRC client), set

    client_cert_path = path/to/p12
    client_cert_pass = THEPASSYOUCHOSE
    
  4. Get the fingerprint of the cert with

    openssl x509 -in myCertificate.crt -outform der | sha512sum -b | cut -d' ' -f1
    

    or from the p12 file with

    openssl pkcs12 -in keyStore.p12 -nodes -passin pass:YOURPASSHERE | openssl x509 -outform der | sha512sum -b | cut -d' ' -f1
    
  5. Add the fingerprint to the bot’s IRC account with

    /msg nickserv add cert YOURFINGERPRINT
    

All set? Not entirely. While this correctly adds the certificate to your connection, it is not enough to get around the SASL restriction. It is enough to get identified in case you can connect without the SASL block kicking in. Sadly it seems that automatic SASL support is not currently (v0.15) supported by this IRC library. Instead, we need to do some work ourselves, which I adapted from this issue.

Go to wherever you are starting your client

let mut client = Client::from_config(config.clone()).await?;
// Send this to ask the server whether it has SASL
client.send_cap_req(&[Capability::Sasl])?;
// Need to set some info so the server knows who you want to identify for
// The IRC crate does this too, but too late for our SASL it seems
client.send(Command::NICK(config.nickname()?.to_string()))?;
// Perhaps use username() here? Just copied from the bug report
client.send(Command::USER(
    config.nickname()?.to_string(),
    "0".to_owned(),
    config.nickname()?.to_string(),
))?;

Then in where you are handling the incoming messages

let mut stream = client.stream()?;

while let Some(irc_msg) = stream.next().await.transpose()? {
    match irc_msg.command {
        Command::CAP(_, ref subcommand, _, _) => {
            if subcommand.to_str() == "ACK" {
                println!("Recieved ack for sasl");
                // Enable external SASL (i.e., via the cert)
                client.send_sasl_external()?;
            }
        }
        Command::AUTHENTICATE(_) => {
            println!("Got signal to continue authenticating");
            // The + seems to be a sort of "look at my cert"
            client.send(Command::AUTHENTICATE(String::from('+')))?;
            client.send(Command::CAP(None, "END".parse()?, None, None))?;
        }
        Command::Response(code, _) => {
            if code == Response::RPL_SASLSUCCESS {
                println!("Successfully authenticated");
                client.send(Command::CAP(None, "END".parse()?, None, None))?;
            }
        }
        _ => {}
    };
}

If it all goes correctly, you should get a 900 (“you are now logged in as…”) and a 903 (“SASL authentication successful”) response.

That was a Saturday afternoon ruined, but it seems to be working for me. For now.