From 4e65b76c67d585dc65b23f6585d7c6e32e21fa51 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 11 Mar 2026 22:41:49 -0700 Subject: [PATCH] fix #2353 (#2354) Check certfp in API /v1/check_auth --- docs/API.md | 3 +++ irc/accounts.go | 43 +++++++++++++++++++++++++++++++------------ irc/api.go | 38 +++++++++++++++++++++----------------- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/docs/API.md b/docs/API.md index 1201f737..b0872978 100644 --- a/docs/API.md +++ b/docs/API.md @@ -46,6 +46,9 @@ This endpoint verifies the credentials of a NickServ account; this allows Ergo t * `accountName`: string, name of the account * `passphrase`: string, alleged passphrase of the account +* `certfp`: string, alleged certificate fingerprint (hex-encoded SHA-256 checksum of the decoded raw certificate) associated with the account + +Each individual field is optional, since a user may be authenticated either by account-passphrase pair or by certificate. The response is a JSON object with fields: diff --git a/irc/accounts.go b/irc/accounts.go index df59d230..a366d7fb 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -9,6 +9,7 @@ import ( "crypto/x509" "encoding/json" "fmt" + "net" "sort" "strconv" "strings" @@ -2039,12 +2040,6 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin defer func() { if err != nil { return - } else if !clientAccount.Verified { - err = errAccountUnverified - return - } else if clientAccount.Suspended != nil { - err = errAccountSuspended - return } // TODO(#1109) clean this check up? if client.registered { @@ -2057,11 +2052,34 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin return }() + clientAccount, err = am.checkCertAuth(client.IP(), certfp, peerCerts, authzid) + return err +} + +func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x509.Certificate, authzid string) (clientAccount ClientAccount, err error) { + defer func() { + if err != nil { + return + } else if !clientAccount.Verified { + err = errAccountUnverified + return + } else if clientAccount.Suspended != nil { + err = errAccountSuspended + return + } + }() + config := am.server.Config() if config.Accounts.AuthScript.Enabled { var output AuthScriptOutput - output, err = CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig, - AuthScriptInput{Certfp: certfp, IP: client.IP().String(), peerCerts: peerCerts}) + var ipString string + if ip != nil { + ipString = ip.String() + } + output, err = CheckAuthScript( + am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig, + AuthScriptInput{Certfp: certfp, IP: ipString, peerCerts: peerCerts}, + ) if err != nil { am.server.logger.Error("internal", "failed shell auth invocation", err.Error()) } else if output.Success && output.AccountName != "" { @@ -2082,18 +2100,19 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin }) if err != nil { - return err + return } if authzid != "" { - if cfAuthzid, err := CasefoldName(authzid); err != nil || cfAuthzid != account { - return errAuthzidAuthcidMismatch + if cfAuthzid, cErr := CasefoldName(authzid); cErr != nil || cfAuthzid != account { + err = errAuthzidAuthcidMismatch + return } } // ok, we found an account corresponding to their certificate clientAccount, err = am.LoadAccount(account) - return err + return } type settingsMunger func(input AccountSettings) (output AccountSettings, err error) diff --git a/irc/api.go b/irc/api.go index ca31f99e..84063620 100644 --- a/irc/api.go +++ b/irc/api.go @@ -130,25 +130,29 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) { var response apiCheckAuthResponse - // try passphrase if present + var account ClientAccount + var err error + + // try whatever credentials are present if request.AccountName != "" && request.Passphrase != "" { - account, err := a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase) - switch err { - case nil: - // success, no error - response.Success = true - response.AccountName = account.Name - case errAccountDoesNotExist, errAccountInvalidCredentials, errAccountUnverified, errAccountSuspended: - // fail, no error - response.Success = false - default: - response.Success = false - response.Error = err.Error() - } + account, err = a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase) + } else if request.Certfp != "" { + account, err = a.server.accounts.checkCertAuth(nil, request.Certfp, nil, "") + } else { + err = errAccountInvalidCredentials } - // try certfp if present - if !response.Success && request.Certfp != "" { - // TODO support cerftp + + switch err { + case nil: + // success, no error + response.Success = true + response.AccountName = account.Name + case errAccountDoesNotExist, errAccountInvalidCredentials, errAccountUnverified, errAccountSuspended: + // fail, no error + response.Success = false + default: + response.Success = false + response.Error = err.Error() } a.writeJSONResponse(response, w, r)