Add SASL EXTERNAL
This commit is contained in:
parent
831f538a98
commit
a382ac8ad3
3 changed files with 264 additions and 5 deletions
|
|
@ -22,6 +22,10 @@ pub struct Account {
|
||||||
pub scram256: Option<String>,
|
pub scram256: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub scram512: Option<String>,
|
pub scram512: Option<String>,
|
||||||
|
// TLS client-certificate fingerprints (lowercase hex) that may log in to
|
||||||
|
// this account via SASL EXTERNAL. Each fingerprint maps to one account.
|
||||||
|
#[serde(default)]
|
||||||
|
pub certfps: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event-sourced persistence: every change is an Event appended to a JSONL log,
|
// Event-sourced persistence: every change is an Event appended to a JSONL log,
|
||||||
|
|
@ -31,13 +35,31 @@ pub struct Account {
|
||||||
#[serde(tag = "event")]
|
#[serde(tag = "event")]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
AccountRegistered(Account),
|
AccountRegistered(Account),
|
||||||
|
CertAdded { account: String, fp: String },
|
||||||
|
CertRemoved { account: String, fp: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum RegError {
|
pub enum RegError {
|
||||||
Exists,
|
Exists,
|
||||||
Internal,
|
Internal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CertError {
|
||||||
|
Invalid, // not a plausible fingerprint
|
||||||
|
InUse, // already registered (to any account)
|
||||||
|
NoAccount, // target account does not exist
|
||||||
|
Internal, // persistence failed
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fingerprint is hex (hash digest), optionally colon-separated. Bound the
|
||||||
|
// length so a junk value can't bloat an account.
|
||||||
|
fn valid_fp(fp: &str) -> bool {
|
||||||
|
let hex = fp.chars().filter(|c| *c != ':').count();
|
||||||
|
(32..=128).contains(&hex) && fp.chars().all(|c| c.is_ascii_hexdigit() || c == ':')
|
||||||
|
}
|
||||||
|
|
||||||
// The expensive, password-derived half of an account, computed once at
|
// The expensive, password-derived half of an account, computed once at
|
||||||
// registration. Split out from `register` so the derivation (argon2 + two SCRAM
|
// registration. Split out from `register` so the derivation (argon2 + two SCRAM
|
||||||
// verifiers, ~1s at the default cost) can run off the reactor via spawn_blocking.
|
// verifiers, ~1s at the default cost) can run off the reactor via spawn_blocking.
|
||||||
|
|
@ -72,6 +94,16 @@ impl Db {
|
||||||
Ok(Event::AccountRegistered(a)) => {
|
Ok(Event::AccountRegistered(a)) => {
|
||||||
accounts.insert(key(&a.name), a);
|
accounts.insert(key(&a.name), a);
|
||||||
}
|
}
|
||||||
|
Ok(Event::CertAdded { account, fp }) => {
|
||||||
|
if let Some(a) = accounts.get_mut(&key(&account)) {
|
||||||
|
a.certfps.push(fp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::CertRemoved { account, fp }) => {
|
||||||
|
if let Some(a) = accounts.get_mut(&key(&account)) {
|
||||||
|
a.certfps.retain(|c| *c != fp);
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => tracing::warn!(%e, "skipping malformed event log line"),
|
Err(e) => tracing::warn!(%e, "skipping malformed event log line"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +139,7 @@ impl Db {
|
||||||
ts: now(),
|
ts: now(),
|
||||||
scram256: Some(creds.scram256),
|
scram256: Some(creds.scram256),
|
||||||
scram512: Some(creds.scram512),
|
scram512: Some(creds.scram512),
|
||||||
|
certfps: Vec::new(),
|
||||||
};
|
};
|
||||||
self.append(&Event::AccountRegistered(account.clone())).map_err(|_| RegError::Internal)?;
|
self.append(&Event::AccountRegistered(account.clone())).map_err(|_| RegError::Internal)?;
|
||||||
self.accounts.insert(key(name), account);
|
self.accounts.insert(key(name), account);
|
||||||
|
|
@ -139,6 +172,50 @@ impl Db {
|
||||||
Some((account.name.as_str(), verifier))
|
Some((account.name.as_str(), verifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The canonical name of the account owning `fp`, if any. Fingerprints are
|
||||||
|
/// globally unique, so this is unambiguous.
|
||||||
|
pub fn certfp_owner(&self, fp: &str) -> Option<&str> {
|
||||||
|
let fp = fp.to_ascii_lowercase();
|
||||||
|
self.accounts.values().find(|a| a.certfps.iter().any(|c| *c == fp)).map(|a| a.name.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fingerprints registered to an account (empty if unknown/none).
|
||||||
|
pub fn certfps(&self, account: &str) -> &[String] {
|
||||||
|
self.accounts.get(&key(account)).map_or(&[], |a| a.certfps.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register `fp` to `account`. Fingerprints are one-to-one with accounts.
|
||||||
|
pub fn certfp_add(&mut self, account: &str, fp: &str) -> Result<(), CertError> {
|
||||||
|
let fp = fp.to_ascii_lowercase();
|
||||||
|
if !valid_fp(&fp) {
|
||||||
|
return Err(CertError::Invalid);
|
||||||
|
}
|
||||||
|
if self.certfp_owner(&fp).is_some() {
|
||||||
|
return Err(CertError::InUse);
|
||||||
|
}
|
||||||
|
let k = key(account);
|
||||||
|
if !self.accounts.contains_key(&k) {
|
||||||
|
return Err(CertError::NoAccount);
|
||||||
|
}
|
||||||
|
self.append(&Event::CertAdded { account: account.to_string(), fp: fp.clone() }).map_err(|_| CertError::Internal)?;
|
||||||
|
self.accounts.get_mut(&k).unwrap().certfps.push(fp);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove `fp` from `account`. Ok(false) if the account had no such fingerprint.
|
||||||
|
pub fn certfp_del(&mut self, account: &str, fp: &str) -> Result<bool, CertError> {
|
||||||
|
let fp = fp.to_ascii_lowercase();
|
||||||
|
let k = key(account);
|
||||||
|
match self.accounts.get(&k) {
|
||||||
|
None => return Err(CertError::NoAccount),
|
||||||
|
Some(a) if !a.certfps.iter().any(|c| *c == fp) => return Ok(false),
|
||||||
|
Some(_) => {}
|
||||||
|
}
|
||||||
|
self.append(&Event::CertRemoved { account: account.to_string(), fp: fp.clone() }).map_err(|_| CertError::Internal)?;
|
||||||
|
self.accounts.get_mut(&k).unwrap().certfps.retain(|c| *c != fp);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
fn append(&self, event: &Event) -> std::io::Result<()> {
|
fn append(&self, event: &Event) -> std::io::Result<()> {
|
||||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
|
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
|
||||||
writeln!(f, "{}", serde_json::to_string(event).unwrap_or_default())
|
writeln!(f, "{}", serde_json::to_string(event).unwrap_or_default())
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use state::Network;
|
||||||
|
|
||||||
// SASL mechanisms we offer, strongest first. Advertised to the uplink as the
|
// SASL mechanisms we offer, strongest first. Advertised to the uplink as the
|
||||||
// `sasl=` capability value (IRCv3 SASL 3.2 mechanism list) and the set we accept.
|
// `sasl=` capability value (IRCv3 SASL 3.2 mechanism list) and the set we accept.
|
||||||
const SASL_MECHS: &str = "SCRAM-SHA-512,SCRAM-SHA-256,PLAIN";
|
const SASL_MECHS: &str = "EXTERNAL,SCRAM-SHA-512,SCRAM-SHA-256,PLAIN";
|
||||||
|
|
||||||
// A client's base64 response is split into chunks of this length; a chunk
|
// A client's base64 response is split into chunks of this length; a chunk
|
||||||
// shorter than this (or a lone "+") marks the end of the response (SASL 3.1).
|
// shorter than this (or a lone "+") marks the end of the response (SASL 3.1).
|
||||||
|
|
@ -31,6 +31,8 @@ enum SaslSession {
|
||||||
Plain { response: String },
|
Plain { response: String },
|
||||||
// SCRAM: which hash, and where we are in the challenge/response.
|
// SCRAM: which hash, and where we are in the challenge/response.
|
||||||
Scram { hash: scram::Hash, step: ScramStep },
|
Scram { hash: scram::Hash, step: ScramStep },
|
||||||
|
// EXTERNAL: the TLS cert fingerprints the ircd relayed for this client.
|
||||||
|
External { fingerprints: Vec<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
// An in-progress exchange with its last-activity stamp, so abandoned sessions
|
// An in-progress exchange with its last-activity stamp, so abandoned sessions
|
||||||
|
|
@ -169,6 +171,12 @@ impl Engine {
|
||||||
self.stash_sasl(client.clone(), SaslSession::Plain { response: String::new() });
|
self.stash_sasl(client.clone(), SaslSession::Plain { response: String::new() });
|
||||||
mk("C", vec!["+".to_string()]) // empty challenge -> client sends the payload
|
mk("C", vec!["+".to_string()]) // empty challenge -> client sends the payload
|
||||||
}
|
}
|
||||||
|
Some("EXTERNAL") => {
|
||||||
|
// The ircd appends the client's TLS cert fingerprints after the mechanism.
|
||||||
|
let fingerprints = data.iter().skip(1).map(|f| f.to_ascii_lowercase()).collect();
|
||||||
|
self.stash_sasl(client.clone(), SaslSession::External { fingerprints });
|
||||||
|
mk("C", vec!["+".to_string()]) // client sends its authzid (or "+")
|
||||||
|
}
|
||||||
Some(mech) if scram::Hash::from_mech(mech).is_some() => {
|
Some(mech) if scram::Hash::from_mech(mech).is_some() => {
|
||||||
let hash = scram::Hash::from_mech(mech).unwrap();
|
let hash = scram::Hash::from_mech(mech).unwrap();
|
||||||
self.stash_sasl(client.clone(), SaslSession::Scram { hash, step: ScramStep::ClientFirst });
|
self.stash_sasl(client.clone(), SaslSession::Scram { hash, step: ScramStep::ClientFirst });
|
||||||
|
|
@ -200,6 +208,7 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(SaslSession::Scram { hash, step }) => self.sasl_scram(&agent, &client, hash, step, chunk),
|
Some(SaslSession::Scram { hash, step }) => self.sasl_scram(&agent, &client, hash, step, chunk),
|
||||||
|
Some(SaslSession::External { fingerprints }) => self.sasl_external(&agent, &client, fingerprints, chunk),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"D" => {
|
"D" => {
|
||||||
|
|
@ -263,6 +272,27 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SASL EXTERNAL: the credential is the client's TLS cert fingerprint, which
|
||||||
|
// the ircd handed us in the S message (already validated at the handshake).
|
||||||
|
// Match it to an account; an authzid, if given, must name that same account.
|
||||||
|
fn sasl_external(&self, agent: &str, client: &str, fingerprints: Vec<String>, chunk: &str) -> Vec<NetAction> {
|
||||||
|
let authzid = if chunk == "+" || chunk.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
match STANDARD.decode(chunk).ok().and_then(|b| String::from_utf8(b).ok()) {
|
||||||
|
Some(a) => a,
|
||||||
|
None => return sasl_fail(agent, client),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match fingerprints.iter().find_map(|fp| self.db.certfp_owner(fp)) {
|
||||||
|
Some(account) if authzid.is_empty() || authzid.eq_ignore_ascii_case(account) => {
|
||||||
|
let account = account.to_string();
|
||||||
|
sasl_success(agent, client, account)
|
||||||
|
}
|
||||||
|
_ => sasl_fail(agent, client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Authority side of the IRCv3 account-registration relay. Hands the work to
|
// Authority side of the IRCv3 account-registration relay. Hands the work to
|
||||||
// the link layer (which derives the password off-thread) via DeferRegister.
|
// the link layer (which derives the password off-thread) via DeferRegister.
|
||||||
fn account_request(&mut self, reqid: String, kind: String, account: String, p2: String, p3: String) -> Vec<NetAction> {
|
fn account_request(&mut self, reqid: String, kind: String, account: String, p2: String, p3: String) -> Vec<NetAction> {
|
||||||
|
|
@ -330,6 +360,11 @@ fn login_plain(b64: &str, db: &Db) -> Option<String> {
|
||||||
db.authenticate(authcid, passwd).map(str::to_string)
|
db.authenticate(authcid, passwd).map(str::to_string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report SASL failure to the ircd (drives 904).
|
||||||
|
fn sasl_fail(agent: &str, client: &str) -> Vec<NetAction> {
|
||||||
|
vec![NetAction::Sasl { agent: agent.to_string(), client: client.to_string(), mode: "D".to_string(), data: vec!["F".to_string()] }]
|
||||||
|
}
|
||||||
|
|
||||||
// The two actions that complete any mechanism: set the account (drives 900),
|
// The two actions that complete any mechanism: set the account (drives 900),
|
||||||
// then report SASL success (drives 903).
|
// then report SASL success (drives 903).
|
||||||
fn sasl_success(agent: &str, client: &str, account: String) -> Vec<NetAction> {
|
fn sasl_success(agent: &str, client: &str, account: String) -> Vec<NetAction> {
|
||||||
|
|
@ -603,4 +638,87 @@ mod tests {
|
||||||
e.handle(NetEvent::Quit { uid: "ZZZ".into() });
|
e.handle(NetEvent::Quit { uid: "ZZZ".into() });
|
||||||
assert!(!e.sasl_sessions.contains_key("ZZZ"), "quit should drop the exchange");
|
assert!(!e.sasl_sessions.contains_key("ZZZ"), "quit should drop the exchange");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drive a SASL step with a multi-field data vector (EXTERNAL carries the
|
||||||
|
// mechanism plus the ircd-supplied fingerprints in one message).
|
||||||
|
fn sasl_multi(e: &mut Engine, mode: &str, data: &[&str]) -> Vec<NetAction> {
|
||||||
|
e.handle(NetEvent::Sasl {
|
||||||
|
client: "000AAAAAB".into(),
|
||||||
|
agent: "*".into(),
|
||||||
|
mode: mode.into(),
|
||||||
|
data: data.iter().map(|s| s.to_string()).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXTERNAL with a registered fingerprint logs in; matching is case-insensitive.
|
||||||
|
#[test]
|
||||||
|
fn external_success_with_registered_cert() {
|
||||||
|
let mut e = engine_with("extok", "foo", "sesame");
|
||||||
|
e.db.certfp_add("foo", "aabbccddeeff00112233445566778899").unwrap();
|
||||||
|
assert_eq!(datum(&sasl_multi(&mut e, "S", &["EXTERNAL", "AABBCCDDEEFF00112233445566778899"])), ("C", "+"));
|
||||||
|
assert!(is_success(&sasl(&mut e, "C", "+")), "empty authzid should log in");
|
||||||
|
}
|
||||||
|
|
||||||
|
// An authzid, if sent, must name the cert's own account.
|
||||||
|
#[test]
|
||||||
|
fn external_authzid_must_match() {
|
||||||
|
let mut e = engine_with("extaz", "foo", "sesame");
|
||||||
|
e.db.certfp_add("foo", "aabbccddeeff00112233445566778899").unwrap();
|
||||||
|
sasl_multi(&mut e, "S", &["EXTERNAL", "aabbccddeeff00112233445566778899"]);
|
||||||
|
assert!(is_success(&sasl(&mut e, "C", &STANDARD.encode("foo"))), "matching authzid ok");
|
||||||
|
|
||||||
|
let mut e = engine_with("extaz2", "foo", "sesame");
|
||||||
|
e.db.certfp_add("foo", "aabbccddeeff00112233445566778899").unwrap();
|
||||||
|
sasl_multi(&mut e, "S", &["EXTERNAL", "aabbccddeeff00112233445566778899"]);
|
||||||
|
assert_eq!(datum(&sasl(&mut e, "C", &STANDARD.encode("bar"))), ("D", "F"), "foreign authzid rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unknown fingerprint, or no fingerprint at all (plaintext client), fails.
|
||||||
|
#[test]
|
||||||
|
fn external_without_registered_cert_fails() {
|
||||||
|
let mut e = engine_with("extno", "foo", "sesame");
|
||||||
|
sasl_multi(&mut e, "S", &["EXTERNAL", "0011223344556677889900aabbccddee"]);
|
||||||
|
assert_eq!(datum(&sasl(&mut e, "C", "+")), ("D", "F"), "unknown fp rejected");
|
||||||
|
|
||||||
|
sasl_multi(&mut e, "S", &["EXTERNAL"]); // no fingerprint supplied
|
||||||
|
assert_eq!(datum(&sasl(&mut e, "C", "+")), ("D", "F"), "no cert rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// End to end: enrol a cert with NickServ CERT ADD, then log in with EXTERNAL.
|
||||||
|
#[test]
|
||||||
|
fn nickserv_cert_add_enables_external() {
|
||||||
|
let mut e = engine_with("nscert", "foo", "sesame");
|
||||||
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "foo".into() });
|
||||||
|
let fp = "aabbccddeeff00112233445566778899";
|
||||||
|
|
||||||
|
let out = e.handle(NetEvent::Privmsg {
|
||||||
|
from: "000AAAAAB".into(),
|
||||||
|
to: "42SAAAAAA".into(),
|
||||||
|
text: format!("CERT ADD sesame {fp}"),
|
||||||
|
});
|
||||||
|
assert!(out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("Added"))), "{out:?}");
|
||||||
|
|
||||||
|
sasl_multi(&mut e, "S", &["EXTERNAL", fp]);
|
||||||
|
assert!(is_success(&sasl(&mut e, "C", "+")), "external should work after CERT ADD");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wrong password can't enrol a cert, and a fingerprint is one account only.
|
||||||
|
#[test]
|
||||||
|
fn cert_add_is_guarded_and_unique() {
|
||||||
|
let mut e = engine_with("certguard", "foo", "sesame");
|
||||||
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "foo".into() });
|
||||||
|
let fp = "aabbccddeeff00112233445566778899";
|
||||||
|
|
||||||
|
let bad = e.handle(NetEvent::Privmsg {
|
||||||
|
from: "000AAAAAB".into(),
|
||||||
|
to: "42SAAAAAA".into(),
|
||||||
|
text: format!("CERT ADD wrongpw {fp}"),
|
||||||
|
});
|
||||||
|
assert!(bad.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("Invalid password"))), "{bad:?}");
|
||||||
|
assert!(e.db.certfp_owner(fp).is_none(), "bad password must not enrol");
|
||||||
|
|
||||||
|
e.db.certfp_add("foo", fp).unwrap();
|
||||||
|
e.db.register("bar", "sesame", None).unwrap();
|
||||||
|
assert!(matches!(e.db.certfp_add("bar", fp), Err(db::CertError::InUse)), "a fingerprint maps to one account");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::engine::db::Db;
|
use crate::engine::db::{CertError, Db};
|
||||||
use crate::engine::service::{Sender, Service, ServiceCtx};
|
use crate::engine::service::{Sender, Service, ServiceCtx};
|
||||||
use crate::proto::RegReply;
|
use crate::proto::RegReply;
|
||||||
|
|
||||||
|
|
@ -48,11 +48,75 @@ impl Service for NickServ {
|
||||||
None => ctx.notice(me, from.uid, "Invalid password."),
|
None => ctx.notice(me, from.uid, "Invalid password."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("HELP") => {
|
Some("CERT") => self.cert(from, &args, ctx, db),
|
||||||
ctx.notice(me, from.uid, "Commands: REGISTER <password> [email], IDENTIFY <password>.")
|
Some("HELP") => ctx.notice(
|
||||||
}
|
me,
|
||||||
|
from.uid,
|
||||||
|
"Commands: REGISTER <password> [email], IDENTIFY <password>, CERT ADD|DEL|LIST <password> [fingerprint].",
|
||||||
|
),
|
||||||
Some(other) => ctx.notice(me, from.uid, format!("Unknown command: {}. Try HELP.", other)),
|
Some(other) => ctx.notice(me, from.uid, format!("Unknown command: {}. Try HELP.", other)),
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl NickServ {
|
||||||
|
// CERT ADD|DEL|LIST <password> [fingerprint]: manage the TLS certificate
|
||||||
|
// fingerprints that may log in to your account via SASL EXTERNAL. Gated by
|
||||||
|
// the account password, so it needs no separate identified-session state.
|
||||||
|
fn cert(&self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) {
|
||||||
|
let me = self.uid.as_str();
|
||||||
|
// All subcommands authenticate the sender's nick's account by password.
|
||||||
|
let auth = |db: &Db, password: &str| db.authenticate(from.nick, password).map(str::to_string);
|
||||||
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
Some("LIST") => {
|
||||||
|
let Some(password) = args.get(2) else {
|
||||||
|
ctx.notice(me, from.uid, "Syntax: CERT LIST <password>");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(account) = auth(db, password) else {
|
||||||
|
ctx.notice(me, from.uid, "Invalid password.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let fps = db.certfps(&account);
|
||||||
|
if fps.is_empty() {
|
||||||
|
ctx.notice(me, from.uid, "No certificate fingerprints are registered to your account.");
|
||||||
|
} else {
|
||||||
|
ctx.notice(me, from.uid, format!("Registered fingerprints: {}", fps.join(", ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("ADD") => {
|
||||||
|
let (Some(password), Some(fp)) = (args.get(2), args.get(3)) else {
|
||||||
|
ctx.notice(me, from.uid, "Syntax: CERT ADD <password> <fingerprint>");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(account) = auth(db, password) else {
|
||||||
|
ctx.notice(me, from.uid, "Invalid password.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match db.certfp_add(&account, fp) {
|
||||||
|
Ok(()) => ctx.notice(me, from.uid, format!("Added fingerprint \x02{fp}\x02. You can now log in with SASL EXTERNAL.")),
|
||||||
|
Err(CertError::InUse) => ctx.notice(me, from.uid, "That fingerprint is already registered."),
|
||||||
|
Err(CertError::Invalid) => ctx.notice(me, from.uid, "That does not look like a certificate fingerprint."),
|
||||||
|
Err(CertError::NoAccount | CertError::Internal) => ctx.notice(me, from.uid, "Could not add the fingerprint, please try again later."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("DEL") | Some("REMOVE") => {
|
||||||
|
let (Some(password), Some(fp)) = (args.get(2), args.get(3)) else {
|
||||||
|
ctx.notice(me, from.uid, "Syntax: CERT DEL <password> <fingerprint>");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(account) = auth(db, password) else {
|
||||||
|
ctx.notice(me, from.uid, "Invalid password.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match db.certfp_del(&account, fp) {
|
||||||
|
Ok(true) => ctx.notice(me, from.uid, format!("Removed fingerprint \x02{fp}\x02.")),
|
||||||
|
Ok(false) => ctx.notice(me, from.uid, "No such fingerprint is registered to your account."),
|
||||||
|
Err(_) => ctx.notice(me, from.uid, "Could not remove the fingerprint, please try again later."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ctx.notice(me, from.uid, "Syntax: CERT ADD|DEL|LIST <password> [fingerprint]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue