3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-14 07:59:31 +01:00

Merge pull request #697 from slingamn/issue696_opercertfp.1

fix #696
This commit is contained in:
Shivaram Lingamneni 2019-12-19 18:34:52 -05:00 committed by GitHub
commit 8720287f3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 139 additions and 36 deletions

View File

@ -1354,3 +1354,18 @@ func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) {
delete(client.invitedTo, casefoldedChannel) delete(client.invitedTo, casefoldedChannel)
return return
} }
// Implements auto-oper by certfp (scans for an auto-eligible operator block that matches
// the client's cert, then applies it).
func (client *Client) attemptAutoOper(session *Session) {
if client.certfp == "" || client.HasMode(modes.Operator) {
return
}
for _, oper := range client.server.Config().operators {
if oper.Auto && oper.Pass == nil && utils.CertfpsMatch(oper.Fingerprint, client.certfp) {
rb := NewResponseBuffer(session)
applyOper(client, oper, rb)
rb.Send(true)
}
}
}

View File

@ -206,7 +206,7 @@ func init() {
}, },
"OPER": { "OPER": {
handler: operHandler, handler: operHandler,
minParams: 2, minParams: 1,
}, },
"PART": { "PART": {
handler: partHandler, handler: partHandler,

View File

@ -211,6 +211,8 @@ type OperConfig struct {
Vhost string Vhost string
WhoisLine string `yaml:"whois-line"` WhoisLine string `yaml:"whois-line"`
Password string Password string
Fingerprint string
Auto bool
Modes string Modes string
} }
@ -459,6 +461,8 @@ type Oper struct {
WhoisLine string WhoisLine string
Vhost string Vhost string
Pass []byte Pass []byte
Fingerprint string
Auto bool
Modes []modes.ModeChange Modes []modes.ModeChange
} }
@ -475,9 +479,17 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
} }
oper.Name = name oper.Name = name
if opConf.Password != "" {
oper.Pass, err = decodeLegacyPasswordHash(opConf.Password) oper.Pass, err = decodeLegacyPasswordHash(opConf.Password)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("Oper %s has an invalid password hash: %s", oper.Name, err.Error())
}
}
oper.Fingerprint = opConf.Fingerprint
oper.Auto = opConf.Auto
if oper.Pass == nil && oper.Fingerprint == "" {
return nil, fmt.Errorf("Oper %s has neither a password nor a fingerprint", name)
} }
oper.Vhost = opConf.Vhost oper.Vhost = opConf.Vhost

View File

@ -2170,28 +2170,50 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
return false return false
} }
// OPER <name> <password> // OPER <name> [password]
func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
if client.HasMode(modes.Operator) { if client.HasMode(modes.Operator) {
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), "OPER", client.t("You're already opered-up!")) rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), "OPER", client.t("You're already opered-up!"))
return false return false
} }
authorized := false // must pass at least one check, and all enabled checks
var checkPassed, checkFailed bool
oper := server.GetOperator(msg.Params[0]) oper := server.GetOperator(msg.Params[0])
if oper != nil { if oper != nil {
password := []byte(msg.Params[1]) if oper.Fingerprint != "" {
authorized = (bcrypt.CompareHashAndPassword(oper.Pass, password) == nil) if utils.CertfpsMatch(oper.Fingerprint, client.certfp) {
checkPassed = true
} else {
checkFailed = true
} }
if !authorized { }
if !checkFailed && oper.Pass != nil {
if len(msg.Params) == 1 || bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil {
checkFailed = true
} else {
checkPassed = true
}
}
}
if !checkPassed || checkFailed {
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect")) rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect"))
client.Quit(client.t("Password incorrect"), rb.session) client.Quit(client.t("Password incorrect"), rb.session)
return true return true
} }
oldNickmask := client.NickMaskString() applyOper(client, oper, rb)
return false
}
// applies operator status to a client, who MUST NOT already be an operator
func applyOper(client *Client, oper *Oper, rb *ResponseBuffer) {
details := client.Details()
oldNickmask := details.nickMask
client.SetOper(oper) client.SetOper(oper)
if client.NickMaskString() != oldNickmask { newNickmask := client.NickMaskString()
if newNickmask != oldNickmask {
client.sendChghost(oldNickmask, oper.Vhost) client.sendChghost(oldNickmask, oper.Vhost)
} }
@ -2204,17 +2226,14 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
copy(modeChanges[1:], oper.Modes) copy(modeChanges[1:], oper.Modes)
applied := ApplyUserModeChanges(client, modeChanges, true) applied := ApplyUserModeChanges(client, modeChanges, true)
rb.Add(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator")) client.server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name))
rb.Add(nil, server.name, "MODE", client.nick, applied.String())
server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name)) rb.Broadcast(nil, client.server.name, RPL_YOUREOPER, details.nick, client.t("You are now an IRC operator"))
rb.Broadcast(nil, client.server.name, "MODE", details.nick, applied.String())
// client may now be unthrottled by the fakelag system
for _, session := range client.Sessions() { for _, session := range client.Sessions() {
// client may now be unthrottled by the fakelag system
session.resetFakelag() session.resetFakelag()
} }
return false
} }
// PART <channel>{,<channel>} [<reason>] // PART <channel>{,<channel>} [<reason>]
@ -2641,7 +2660,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil { if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
continue continue
} }
if 0 < len(info.Fingerprint) && client.certfp != info.Fingerprint { if 0 < len(info.Fingerprint) && !utils.CertfpsMatch(info.Fingerprint, client.certfp) {
continue continue
} }

View File

@ -349,7 +349,7 @@ The NPC command is used to send an action to the target as the source.
Requires the roleplay mode (+E) to be set on the target.`, Requires the roleplay mode (+E) to be set on the target.`,
}, },
"oper": { "oper": {
text: `OPER <name> <password> text: `OPER <name> [password]
If the correct details are given, gives you IRCop privs.`, If the correct details are given, gives you IRCop privs.`,
}, },

View File

@ -77,6 +77,18 @@ func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command str
rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...)) rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
} }
// Broadcast adds a standard new message to our queue, then sends an unlabeled copy
// to all other sessions.
func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, command string, params ...string) {
// can't reuse the IrcMessage object because of tag pollution :-\
rb.Add(tags, prefix, command, params...)
for _, session := range rb.session.client.Sessions() {
if session != rb.session {
session.Send(tags, prefix, command, params...)
}
}
}
// AddFromClient adds a new message from a specific client to our queue. // AddFromClient adds a new message from a specific client to our queue.
func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) { func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...) msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)

View File

@ -440,6 +440,9 @@ func (server *Server) playRegistrationBurst(session *Session) {
if modestring != "+" { if modestring != "+" {
session.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, modestring) session.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, modestring)
} }
c.attemptAutoOper(session)
if server.logger.IsLoggingRawIO() { if server.logger.IsLoggingRawIO() {
session.Send(nil, c.server.name, "NOTICE", d.nick, c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) session.Send(nil, c.server.name, "NOTICE", d.nick, c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
} }

View File

@ -8,6 +8,7 @@ import (
"crypto/subtle" "crypto/subtle"
"encoding/base32" "encoding/base32"
"encoding/base64" "encoding/base64"
"strings"
) )
var ( var (
@ -68,3 +69,15 @@ func GenerateSecretKey() string {
rand.Read(buf[:]) rand.Read(buf[:])
return base64.RawURLEncoding.EncodeToString(buf[:]) return base64.RawURLEncoding.EncodeToString(buf[:])
} }
func normalizeCertfp(certfp string) string {
return strings.ToLower(strings.Replace(certfp, ":", "", -1))
}
// Convenience to compare certfps as returned by different tools, e.g., openssl vs. oragono
func CertfpsMatch(storedCertfp, suppliedCertfp string) bool {
if storedCertfp == "" {
return false
}
return normalizeCertfp(storedCertfp) == normalizeCertfp(suppliedCertfp)
}

View File

@ -81,3 +81,21 @@ func BenchmarkMungeSecretToken(b *testing.B) {
t = MungeSecretToken(t) t = MungeSecretToken(t)
} }
} }
func TestCertfpComparisons(t *testing.T) {
opensslFP := "3D:6B:11:BF:B4:05:C3:F8:4B:38:CD:30:38:FB:EC:01:71:D5:03:54:79:04:07:88:4C:A5:5D:23:41:85:66:C9"
oragonoFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c9"
badFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c8"
if !CertfpsMatch(opensslFP, oragonoFP) {
t.Error("these certs should match")
}
if !CertfpsMatch(oragonoFP, opensslFP) {
t.Error("these certs should match")
}
if CertfpsMatch("", "") {
t.Error("empty stored certfp should not match empty provided certfp")
}
if CertfpsMatch(opensslFP, badFP) {
t.Error("these certs should not match")
}
}

View File

@ -120,8 +120,9 @@ server:
webirc: webirc:
# one webirc block -- should correspond to one set of gateways # one webirc block -- should correspond to one set of gateways
- -
# tls fingerprint the gateway must connect with to use this webirc block # SHA-256 fingerprint of the TLS certificate the gateway must use to connect
fingerprint: 938dd33f4b76dcaf7ce5eb25c852369cb4b8fb47ba22fc235aa29c6623a5f182 # (comment this out to use passwords only)
fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
# password the gateway uses to connect, made with oragono genpasswd # password the gateway uses to connect, made with oragono genpasswd
password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee" password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee"
@ -445,10 +446,20 @@ opers:
# modes are the modes to auto-set upon opering-up # modes are the modes to auto-set upon opering-up
modes: +is acjknoqtux modes: +is acjknoqtux
# password to login with /OPER command # operators can be authenticated either by password (with the /OPER command),
# generated using "oragono genpasswd" # or by certificate fingerprint, or both. if a password hash is set, then a
# password is required to oper up (e.g., /OPER dan mypassword). to generate
# the hash, use `oragono genpasswd`.
password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu" password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu"
# if a SHA-256 certificate fingerprint is configured here, then it will be
# required to /OPER. if you comment out the password hash above, then you can
# /OPER without a password.
#fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
# if 'auto' is set (and no password hash is set), operator permissions will be
# granted automatically as soon as you connect with the right fingerprint.
#auto: true
# logging, takes inspiration from Insp # logging, takes inspiration from Insp
logging: logging:
- -